Add find_or_create_pull_request action#733
Conversation
Adds a generic action that returns the URL of the open Pull Request for a head branch, creating one only if none exists yet. Backed by a new GithubHelper#find_pull_request (list-by-head). Useful for rolling automations (e.g. a daily translations job) that force-push the same head branch each run, where GitHub auto-refreshes the existing PR's diff.
There was a problem hiding this comment.
Pull request overview
Adds a new Fastlane action to de-duplicate “rolling” pull request creation by reusing an existing open PR for a given head branch (and optionally base), backed by a new GithubHelper#find_pull_request helper and accompanying specs.
Changes:
- Added
Fastlane::Actions::FindOrCreatePullRequestActionto find an existing open PR for a head branch or create one and return its URL. - Added
Fastlane::Helper::GithubHelper#find_pull_requestthat queries open PRs by qualifiedhead(and optionalbase). - Added/updated RSpec coverage for the new helper method and action behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
spec/github_helper_spec.rb |
Adds unit tests for GithubHelper#find_pull_request head qualification and filtering behavior. |
spec/find_or_create_pull_request_spec.rb |
Adds unit tests validating “find existing PR vs create PR” behavior and parameter forwarding. |
lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb |
Introduces #find_pull_request helper using Octokit list-by-head (and optional base). |
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/find_or_create_pull_request_action.rb |
New action orchestrating lookup then delegating to Fastlane create_pull_request when needed. |
CHANGELOG.md |
Documents the new action/helper under Trunk “New Features”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Looks for an open Pull Request whose head is the given branch (optionally targeting a specific base), | ||
| and returns its URL if found. Otherwise, creates a new Pull Request and returns its URL. |
| # @param [String] repository The repository name, including the organization (e.g. `wordpress-mobile/wordpress-ios`) | ||
| # @param [String] head The head branch to look for. May be given as `branch` or as the fully-qualified `owner:branch`; | ||
| # when unqualified, it is automatically prefixed with the repository's owner. | ||
| # @param [String] base The base branch the PR should target. If nil, PRs targeting any base are considered. |
- Reword the action's details: base is required (drop 'optionally targeting a specific base') to match the required `base` ConfigItem. - find_pull_request YARD: mark `base` as `[String?]` since it accepts nil.
| end | ||
|
|
||
| it 'qualifies an unqualified head with the repository owner and forwards the base' do | ||
| expect(client).to receive(:pull_requests).with(test_repo, { state: 'open', head: 'repo-test:my-branch', base: 'trunk' }) |
There was a problem hiding this comment.
Claude: Leaving this as-is. These tests rely on the instance_double(Octokit::Client, pull_requests: [found_pr], …) default return, so client.pull_requests(...) returns [found_pr] and .first never hits nil (the suite is green — it would raise otherwise). This matches the existing pattern in this file, e.g. expect(client).to receive(:list_milestones) paired with an instance_double default. Adding .and_return onto the expect would also conflict with rubocop's RSpec/StubbedMock.
| end | ||
|
|
||
| it 'uses an already-qualified head as-is and omits the base when not provided' do | ||
| expect(client).to receive(:pull_requests).with(test_repo, { state: 'open', head: 'someone:other-branch' }) |
There was a problem hiding this comment.
Claude: Leaving this as-is. These tests rely on the instance_double(Octokit::Client, pull_requests: [found_pr], …) default return, so client.pull_requests(...) returns [found_pr] and .first never hits nil (the suite is green — it would raise otherwise). This matches the existing pattern in this file, e.g. expect(client).to receive(:list_milestones) paired with an instance_double default. Adding .and_return onto the expect would also conflict with rubocop's RSpec/StubbedMock.
| def self.run(params) | ||
| github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token]) | ||
|
|
||
| existing_pr = github_helper.find_pull_request( |
There was a problem hiding this comment.
api_url is used on create_pull_request but not here. I guess we'd need to change the helper for that but it probably makes sense, for consistency?
There was a problem hiding this comment.
My understanding is that adding api_url requires changing a shared helper used across ~14 call sites, and a behavior change for create_release_backmerge_pull_request_action which will now call the custom host for @client.user calls. That, in my opinion, requires its own PR.
Since this PR follows the existing create_release_backmerge_pull_request_action from the repo, I'd prefer to leave that to a follow up if you don't mind.
| return existing_pr.html_url | ||
| end | ||
|
|
||
| other_action.create_pull_request( |
There was a problem hiding this comment.
💭 nit: this call supports creating drafts, we could forward a draft parameter to allow for creating a draft PR. Not a big deal and we can address it in a follow-up.
iangmaia
left a comment
There was a problem hiding this comment.
Thanks for implementing this, sounds like a useful addition 👍
Apologies for not already approving it yesterday but I was already at the end of my day.
Allow creating draft PRs by forwarding a draft option to the underlying create_pull_request action.
What does it do?
Adds a generic action that returns the URL of the open Pull Request for a head branch, creating one only if none exists yet. Backed by a new
GithubHelper#find_pull_request(list-by-head).Useful for "rolling" automations (e.g. a daily translations or dependency-update job) that force-push the same head branch each run — GitHub auto-refreshes the existing PR's diff, so a PR only needs to be opened the first time. Token handling uses the standard
github_token_config_item, and PR parameters are forwarded tocreate_pull_request.Checklist before requesting a review
bundle exec rubocopto test for code style violations and recommendations.specs/*_spec.rb) if applicable.bundle exec rspecto run the whole test suite and ensure all your tests pass.CHANGELOG.mdfile to describe your changes under the appropriate existing###subsection of the existing## Trunksection.MIGRATION.mdfile to describe how the changes will affect the migration from the previous major version and what the clients will need to change and consider.