Skip to content

Commit b8c21fc

Browse files
authored
fix: use webhook payload SHAs in list_changed_files to avoid race condition (#1107)
<h3>PR Summary by Qodo</h3> Fix changed-files diff race by using webhook payload base/head SHAs <code>🐞 Bug fix</code> <code>🧪 Tests</code> <code>🕐 20-40 Minutes</code> <img src="https://www.qodo.ai/wp-content/uploads/2025/11/light-grey-line.svg" height="10%" alt="Grey Divider"> <h3>Walkthroughs</h3> <details open> <summary>User Description</summary> <br/> ## Summary Replace live PyGithub API calls with webhook payload SHAs in `list_changed_files()` to eliminate a race condition where base branch receives new commits between clone and API call. - Prefer webhook payload SHAs for `pull_request` events (no race condition) - Fall back to PullRequest API object for non-PR events (issue_comment, check_run, etc.) - Store `pr_base_sha`/`pr_head_sha` on GithubWebhook instance during `process()` - Remove `pull_request` parameter from `initialize()` and `list_changed_files()` - Add symmetric guards for both base and head SHA validation Closes #1096 </details> <details open> <summary>AI Description</summary> <br/> <pre> • Persist PR base/head SHAs from webhook payload to prevent base-branch race conditions. • Fall back to PullRequest API SHAs for non-PR webhook event types. • Simplify OWNERS handler initialization by removing PullRequest parameter plumbing. </pre> </details> <details> <summary>Diagram</summary> <br/> ```mermaid graph TD A["GitHub webhook payload"] --> B["GithubWebhook.process"] --> C["Store PR base/head SHAs"] --> D[("Local clone")] --> E["OwnersFileHandler.list_changed_files"] --> F["git diff --name-only"] B --> G["PullRequest API (fallback)"] --> C ``` </details> <details> <summary>High-Level Assessment</summary> <br/> Using webhook payload SHAs for pull_request events is the most reliable way to keep the local clone and diff base/head aligned and eliminate the observed race. Alternatives like always querying live PR/base refs or diffing against the current base branch would reintroduce timing drift; passing SHAs through additional parameters instead of storing on the per-request GithubWebhook instance would add plumbing without changing the core correctness. </details> <img src="https://www.qodo.ai/wp-content/uploads/2025/11/light-grey-line.svg" height="10%" alt="Grey Divider"> <h3>File Changes</h3> <details> <summary><strong>Bug fix</strong> (2)</summary> <blockquote> <details> <summary><strong>github_api.py</strong> <code>Persist PR base/head SHAs from payload with API fallback</code> <code>+21/-5</code></summary> <br/> >Persist PR base/head SHAs from payload with API fallback > ><pre> >• Stores pr_base_sha/pr_head_sha on the GithubWebhook instance during process(), preferring pull_request payload SHAs to avoid timing drift. Falls back to PullRequest base.sha/head.sha for non-pull_request event payloads and updates OwnersFileHandler initialization calls to the new signature. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1107/files#diff-7c5f6dfcadb38e75c2d0f1d418ba1a861cc9f6c0efe72905a250e9f43a6cfdcf'>webhook_server/libs/github_api.py</a> <hr/> </details> </blockquote> <blockquote> <details> <summary><strong>owners_files_handler.py</strong> <code>Read diff SHAs from GithubWebhook; remove PullRequest parameter</code> <code>+10/-11</code></summary> <br/> >Read diff SHAs from GithubWebhook; remove PullRequest parameter > ><pre> >• Removes the PullRequest parameter from initialize() and list_changed_files(). list_changed_files() now reads base/head SHAs from github_webhook.pr_base_sha/pr_head_sha and documents the race-condition rationale. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1107/files#diff-1f6f88363e42edc5aeaab6ac0422002e56c81ae51f8f3f83c1cd3610813b8c3b'>webhook_server/libs/handlers/owners_files_handler.py</a> <hr/> </details> </blockquote> </details> <details> <summary><strong>Tests</strong> (2)</summary> <blockquote> <details> <summary><strong>test_github_api.py</strong> <code>Add tests for SHA storage from payload and API fallback</code> <code>+107/-0</code></summary> <br/> >Add tests for SHA storage from payload and API fallback > ><pre> >• Introduces coverage verifying payload SHAs are preferred for pull_request events and that non-PR events fall back to PullRequest API SHAs. Mocks cloning/handler initialization to focus assertions on SHA persistence behavior. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1107/files#diff-77f140ed13dff3ea105ea6374a5716a034664a753eafed149f305baf862006f4'>webhook_server/tests/test_github_api.py</a> <hr/> </details> </blockquote> <blockquote> <details> <summary><strong>test_owners_files_handler.py</strong> <code>Update tests for new handler signatures and SHA source</code> <code>+8/-10</code></summary> <br/> >Update tests for new handler signatures and SHA source > ><pre> >• Updates initialize() and list_changed_files() tests to match the removed PullRequest parameter. Adjusts list_changed_files test setup to provide SHAs via the mocked GithubWebhook instance. ></pre> > ><a href='https://github.com/myk-org/github-webhook-server/pull/1107/files#diff-a5967fb188d386a8c95bfa003af701b6d3c0ed7f85fd2a5ea9e16d4483032898'>webhook_server/tests/test_owners_files_handler.py</a> <hr/> </details> </blockquote> </details> <img src="https://www.qodo.ai/wp-content/uploads/2025/11/light-grey-line.svg" height="10%" alt="Grey Divider"> <a href="https://www.qodo.ai"><img src="https://www.qodo.ai/wp-content/uploads/2025/03/qodo-logo.svg" width="80" alt="Qodo Logo"></a>
1 parent d5390cc commit b8c21fc

4 files changed

Lines changed: 186 additions & 27 deletions

File tree

webhook_server/libs/github_api.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
run_command,
5757
)
5858

59+
_SHA_PATTERN = re.compile(r"^[0-9a-f]{40}$")
60+
5961

6062
class CountingRequester:
6163
"""
@@ -115,6 +117,8 @@ def __init__(self, hook_data: dict[Any, Any], headers: Headers, logger: logging.
115117
self.repository_full_name: str = hook_data["repository"]["full_name"]
116118
self._bg_tasks: set[Task[Any]] = set()
117119
self.parent_committer: str = ""
120+
self.pr_base_sha: str = ""
121+
self.pr_head_sha: str = ""
118122
self.x_github_delivery: str = headers.get("X-GitHub-Delivery", "")
119123
self.github_event: str = headers["X-GitHub-Event"]
120124
self.config = Config(repository=self.repository_name, logger=self.logger)
@@ -386,6 +390,43 @@ def redact_output(value: str) -> str:
386390
redacted_err = redact_output(err)
387391
self.logger.error(f"{self.log_prefix} Failed to fetch PR {pr_number} ref: {redacted_err}")
388392
raise RuntimeError(f"Failed to fetch PR {pr_number} ref: {redacted_err}")
393+
394+
# Fetch payload SHAs explicitly to handle force-push race condition
395+
# The webhook payload SHAs may differ from the current PR ref if the PR
396+
# was force-pushed between webhook delivery and processing
397+
# Validate SHA format first — reset invalid SHAs so the fetch is skipped
398+
for sha_attr in ("pr_base_sha", "pr_head_sha"):
399+
sha = getattr(self, sha_attr)
400+
if not isinstance(sha, str) or (sha and not _SHA_PATTERN.match(sha)):
401+
self.logger.warning(
402+
f"{self.log_prefix} Invalid {sha_attr} format: {str(sha)[:20]}, will use API fallback"
403+
)
404+
setattr(self, sha_attr, "")
405+
406+
if self.pr_base_sha and self.pr_head_sha:
407+
for sha in (self.pr_base_sha, self.pr_head_sha):
408+
# Check if SHA exists in clone
409+
rc_check, _, _ = await run_command(
410+
command=f"{git_cmd} cat-file -e {sha}^{{commit}}",
411+
log_prefix=self.log_prefix,
412+
verify_stderr=False,
413+
mask_sensitive=self.mask_sensitive,
414+
)
415+
if not rc_check:
416+
self.logger.debug(
417+
f"{self.log_prefix} Payload SHA {sha[:7]} not in clone, fetching explicitly"
418+
)
419+
rc_fetch, _, _ = await run_command(
420+
command=f"{git_cmd} fetch origin {sha}",
421+
log_prefix=self.log_prefix,
422+
redact_secrets=[github_token],
423+
mask_sensitive=self.mask_sensitive,
424+
)
425+
if not rc_fetch:
426+
self.logger.warning(
427+
f"{self.log_prefix} Failed to fetch payload SHA {sha[:7]} — "
428+
f"git diff may fail if this SHA is unreachable"
429+
)
389430
else:
390431
# For push events (tags only - branch pushes skip cloning)
391432
# checkout_ref guaranteed to be non-None by validation at function start
@@ -449,7 +490,7 @@ async def _recheck_merge_eligibility(self, pull_request: PullRequest) -> None:
449490
"""
450491
await self._clone_repository(pull_request=pull_request)
451492
owners_file_handler = OwnersFileHandler(github_webhook=self)
452-
owners_file_handler = await owners_file_handler.initialize(pull_request=pull_request)
493+
owners_file_handler = await owners_file_handler.initialize()
453494
await PullRequestHandler(github_webhook=self, owners_file_handler=owners_file_handler).check_if_can_be_merged(
454495
pull_request=pull_request
455496
)
@@ -596,6 +637,18 @@ async def process(self) -> Any:
596637
self.parent_committer = pull_request.user.login
597638
self.last_committer = getattr(self.last_commit.committer, "login", self.parent_committer)
598639

640+
# Store PR SHAs: prefer webhook payload (avoids race condition with live API)
641+
# For pull_request events, base.sha and head.sha are guaranteed by GitHub webhook spec.
642+
# For other events (issue_comment, check_run), fall back to PullRequest API object.
643+
if self.github_event == "pull_request":
644+
self.pr_base_sha = self.hook_data["pull_request"]["base"]["sha"]
645+
self.pr_head_sha = self.hook_data["pull_request"]["head"]["sha"]
646+
else:
647+
self.pr_base_sha, self.pr_head_sha = await asyncio.gather(
648+
github_api_call(lambda: pull_request.base.sha, logger=self.logger, log_prefix=self.log_prefix),
649+
github_api_call(lambda: pull_request.head.sha, logger=self.logger, log_prefix=self.log_prefix),
650+
)
651+
599652
# Clone repository for local file processing (OWNERS, changed files)
600653
# For check_run, status, and pull_request_review_thread events,
601654
# cloning happens later only when needed (inside their respective handlers)
@@ -604,7 +657,7 @@ async def process(self) -> Any:
604657

605658
if self.github_event == "issue_comment":
606659
owners_file_handler = OwnersFileHandler(github_webhook=self)
607-
owners_file_handler = await owners_file_handler.initialize(pull_request=pull_request)
660+
owners_file_handler = await owners_file_handler.initialize()
608661

609662
await IssueCommentHandler(
610663
github_webhook=self, owners_file_handler=owners_file_handler
@@ -618,7 +671,7 @@ async def process(self) -> Any:
618671

619672
elif self.github_event == "pull_request":
620673
owners_file_handler = OwnersFileHandler(github_webhook=self)
621-
owners_file_handler = await owners_file_handler.initialize(pull_request=pull_request)
674+
owners_file_handler = await owners_file_handler.initialize()
622675

623676
await PullRequestHandler(
624677
github_webhook=self, owners_file_handler=owners_file_handler
@@ -632,7 +685,7 @@ async def process(self) -> Any:
632685

633686
elif self.github_event == "pull_request_review":
634687
owners_file_handler = OwnersFileHandler(github_webhook=self)
635-
owners_file_handler = await owners_file_handler.initialize(pull_request=pull_request)
688+
owners_file_handler = await owners_file_handler.initialize()
636689

637690
await PullRequestReviewHandler(
638691
github_webhook=self, owners_file_handler=owners_file_handler
@@ -678,7 +731,7 @@ async def process(self) -> Any:
678731
await self._clone_repository(pull_request=pull_request)
679732

680733
owners_file_handler = OwnersFileHandler(github_webhook=self)
681-
owners_file_handler = await owners_file_handler.initialize(pull_request=pull_request)
734+
owners_file_handler = await owners_file_handler.initialize()
682735
handled = await CheckRunHandler(
683736
github_webhook=self, owners_file_handler=owners_file_handler
684737
).process_pull_request_check_run_webhook_data(pull_request=pull_request)

webhook_server/libs/handlers/owners_files_handler.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def __init__(self, github_webhook: "GithubWebhook") -> None:
2828
self.log_prefix: str = self.github_webhook.log_prefix
2929
self.repository: Repository = self.github_webhook.repository
3030

31-
async def initialize(self, pull_request: PullRequest) -> "OwnersFileHandler":
31+
async def initialize(self) -> "OwnersFileHandler":
3232
"""Initialize handler with PR data (optimized with parallel operations).
3333
3434
Phase 1: Fetch independent data in parallel (changed files + OWNERS data)
@@ -37,7 +37,7 @@ async def initialize(self, pull_request: PullRequest) -> "OwnersFileHandler":
3737

3838
# Phase 1: Parallel data fetching - independent GitHub API operations
3939
self.changed_files, self.all_repository_approvers_and_reviewers = await asyncio.gather(
40-
self.list_changed_files(pull_request=pull_request),
40+
self.list_changed_files(),
4141
self.get_all_repository_approvers_and_reviewers(),
4242
)
4343

@@ -84,14 +84,15 @@ def allowed_users(self) -> list[str]:
8484
self.logger.debug(f"{self.log_prefix} ROOT allowed users: {_allowed_users}")
8585
return _allowed_users
8686

87-
async def list_changed_files(self, pull_request: PullRequest) -> list[str]:
87+
async def list_changed_files(self) -> list[str]:
8888
"""List changed files in the PR using git diff on cloned repository.
8989
9090
Uses local git diff command instead of GitHub API to reduce API calls.
9191
The repository is already cloned to self.github_webhook.clone_repo_dir.
92-
93-
Args:
94-
pull_request: PyGithub PullRequest object
92+
SHAs are read from the webhook payload to avoid race conditions with
93+
live API calls (base branch may receive new commits between clone and API call).
94+
If the PR is force-pushed between webhook delivery and processing,
95+
_clone_repository() explicitly fetches payload SHAs to ensure they exist.
9596
9697
Returns:
9798
List of changed file paths relative to repository root
@@ -100,11 +101,11 @@ async def list_changed_files(self, pull_request: PullRequest) -> list[str]:
100101
RuntimeError: If git diff command fails
101102
asyncio.CancelledError: Propagates cancellation (never caught)
102103
"""
103-
# Get base and head SHAs (wrap property accesses in github_api_call for retry support)
104-
base_sha, head_sha = await asyncio.gather(
105-
github_api_call(lambda: pull_request.base.sha, logger=self.logger, log_prefix=self.log_prefix),
106-
github_api_call(lambda: pull_request.head.sha, logger=self.logger, log_prefix=self.log_prefix),
107-
)
104+
# SHAs are stored on the GithubWebhook instance during process():
105+
# - From webhook payload for pull_request events (avoids race condition with live API)
106+
# - From PullRequest object for other event types (issue_comment, check_run, etc.)
107+
base_sha = self.github_webhook.pr_base_sha
108+
head_sha = self.github_webhook.pr_head_sha
108109

109110
# Run git diff command on cloned repository
110111
# Quote clone_repo_dir to handle paths with spaces or special characters

webhook_server/tests/test_github_api.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def pull_request_payload(self) -> dict[str, Any]:
3939
"number": 123,
4040
"title": "Test PR",
4141
"user": {"login": "testuser"},
42-
"base": {"ref": "main"},
42+
"base": {"ref": "main", "sha": "base123"},
4343
"head": {"sha": "abc123"},
4444
},
4545
}
@@ -419,6 +419,113 @@ async def test_process_issue_comment_event(
419419
await webhook.process()
420420
mock_process_comment.assert_called_once()
421421

422+
@pytest.mark.asyncio
423+
async def test_pr_sha_storage_from_webhook_payload(
424+
self, pull_request_payload: dict[str, Any], webhook_headers: Headers
425+
) -> None:
426+
"""Test that pr_base_sha/pr_head_sha are stored from webhook payload for pull_request events."""
427+
# Add base SHA to the payload (head SHA already present)
428+
pull_request_payload["pull_request"]["base"]["sha"] = "base-sha-from-payload"
429+
pull_request_payload["pull_request"]["head"]["sha"] = "head-sha-from-payload"
430+
431+
with (
432+
patch.dict(os.environ, {"WEBHOOK_SERVER_DATA_DIR": "webhook_server/tests/manifests"}),
433+
patch("webhook_server.libs.github_api.get_api_with_highest_rate_limit") as mock_api_rate_limit,
434+
patch("webhook_server.libs.github_api.get_repository_github_app_api") as mock_repo_api,
435+
patch("webhook_server.utils.helpers.get_apis_and_tokes_from_config") as mock_get_apis,
436+
patch("webhook_server.libs.config.Config.repository_local_data") as mock_repo_local_data,
437+
patch("webhook_server.libs.github_api.GithubWebhook.add_api_users_to_auto_verified_and_merged_users"),
438+
):
439+
mock_api = Mock()
440+
mock_api.rate_limiting = [100, 5000]
441+
mock_user = Mock()
442+
mock_user.login = "test-user"
443+
mock_api.get_user.return_value = mock_user
444+
mock_api_rate_limit.return_value = (mock_api, "TOKEN", "USER")
445+
mock_repo_api.return_value = Mock()
446+
mock_get_apis.return_value = []
447+
mock_repo_local_data.return_value = {}
448+
449+
webhook = GithubWebhook(hook_data=pull_request_payload, headers=webhook_headers, logger=Mock())
450+
451+
mock_pr = Mock()
452+
mock_pr.draft = False
453+
mock_pr.user.login = "testuser"
454+
mock_pr.base.ref = "main"
455+
# These API SHAs should NOT be used (payload takes priority)
456+
mock_pr.base.sha = "api-base-sha-should-not-be-used"
457+
mock_pr.head.sha = "api-head-sha-should-not-be-used"
458+
mock_commit = Mock()
459+
mock_pr.get_commits.return_value = [mock_commit]
460+
461+
with (
462+
patch.object(webhook, "get_pull_request", return_value=mock_pr),
463+
patch.object(webhook, "_clone_repository", new=AsyncMock(return_value=None)),
464+
patch.object(OwnersFileHandler, "initialize", new=AsyncMock(return_value=None)),
465+
patch(
466+
"webhook_server.libs.handlers.pull_request_handler.PullRequestHandler.process_pull_request_webhook_data",
467+
new=AsyncMock(return_value=None),
468+
),
469+
):
470+
await webhook.process()
471+
472+
# Verify SHAs came from webhook payload, not live API
473+
assert webhook.pr_base_sha == "base-sha-from-payload"
474+
assert webhook.pr_head_sha == "head-sha-from-payload"
475+
476+
@pytest.mark.asyncio
477+
async def test_pr_sha_storage_fallback_for_non_pr_events(self, issue_comment_payload: dict[str, Any]) -> None:
478+
"""Test that pr_base_sha/pr_head_sha fall back to API for non-pull_request events.
479+
480+
issue_comment payloads have no top-level 'pull_request' dict with SHAs,
481+
so the code must fall back to the PullRequest object's base.sha/head.sha.
482+
"""
483+
with (
484+
patch.dict(os.environ, {"WEBHOOK_SERVER_DATA_DIR": "webhook_server/tests/manifests"}),
485+
patch("webhook_server.libs.github_api.get_api_with_highest_rate_limit") as mock_api_rate_limit,
486+
patch("webhook_server.libs.github_api.get_repository_github_app_api") as mock_repo_api,
487+
patch("webhook_server.utils.helpers.get_apis_and_tokes_from_config") as mock_get_apis,
488+
patch("webhook_server.libs.config.Config.repository_local_data") as mock_repo_local_data,
489+
patch("webhook_server.libs.github_api.GithubWebhook.add_api_users_to_auto_verified_and_merged_users"),
490+
):
491+
mock_api = Mock()
492+
mock_api.rate_limiting = [100, 5000]
493+
mock_user = Mock()
494+
mock_user.login = "test-user"
495+
mock_api.get_user.return_value = mock_user
496+
mock_api_rate_limit.return_value = (mock_api, "TOKEN", "USER")
497+
mock_repo_api.return_value = Mock()
498+
mock_get_apis.return_value = []
499+
mock_repo_local_data.return_value = {}
500+
501+
headers = Headers({"X-GitHub-Event": "issue_comment"})
502+
webhook = GithubWebhook(hook_data=issue_comment_payload, headers=headers, logger=Mock())
503+
504+
mock_pr = Mock()
505+
mock_pr.draft = False
506+
mock_pr.user.login = "testuser"
507+
mock_pr.base.ref = "main"
508+
# These API SHAs SHOULD be used (no payload SHAs for issue_comment)
509+
mock_pr.base.sha = "api-base-sha-fallback"
510+
mock_pr.head.sha = "api-head-sha-fallback"
511+
mock_commit = Mock()
512+
mock_pr.get_commits.return_value = [mock_commit]
513+
514+
with (
515+
patch.object(webhook, "get_pull_request", return_value=mock_pr),
516+
patch.object(webhook, "_clone_repository", new=AsyncMock(return_value=None)),
517+
patch.object(OwnersFileHandler, "initialize", new=AsyncMock(return_value=None)),
518+
patch(
519+
"webhook_server.libs.handlers.issue_comment_handler.IssueCommentHandler.process_comment_webhook_data",
520+
new=AsyncMock(return_value=None),
521+
),
522+
):
523+
await webhook.process()
524+
525+
# Verify SHAs came from API fallback (no payload SHAs for issue_comment)
526+
assert webhook.pr_base_sha == "api-base-sha-fallback"
527+
assert webhook.pr_head_sha == "api-head-sha-fallback"
528+
422529
@patch.dict(os.environ, {"WEBHOOK_SERVER_DATA_DIR": "webhook_server/tests/manifests"})
423530
@patch("webhook_server.libs.github_api.get_repository_github_app_api")
424531
@patch("webhook_server.libs.github_api.get_api_with_highest_rate_limit")

webhook_server/tests/test_owners_files_handler.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def owners_file_handler(self, mock_github_webhook: Mock) -> OwnersFileHandler:
3535
return OwnersFileHandler(mock_github_webhook)
3636

3737
@pytest.mark.asyncio
38-
async def test_initialize(self, owners_file_handler: OwnersFileHandler, mock_pull_request: Mock) -> None:
38+
async def test_initialize(self, owners_file_handler: OwnersFileHandler) -> None:
3939
"""Test the initialize method."""
4040
with patch.object(owners_file_handler, "list_changed_files", new=AsyncMock()) as mock_list_files:
4141
with patch.object(
@@ -60,7 +60,7 @@ async def test_initialize(self, owners_file_handler: OwnersFileHandler, mock_pul
6060
mock_get_pr_approvers.return_value = ["user1"]
6161
mock_get_pr_reviewers.return_value = ["user2"]
6262

63-
result = await owners_file_handler.initialize(mock_pull_request)
63+
result = await owners_file_handler.initialize()
6464

6565
assert result == owners_file_handler
6666
assert owners_file_handler.changed_files == ["file1.py", "file2.py"]
@@ -87,13 +87,11 @@ async def test_ensure_initialized_initialized(self, owners_file_handler: OwnersF
8787
owners_file_handler._ensure_initialized() # Should not raise
8888

8989
@pytest.mark.asyncio
90-
async def test_list_changed_files(
91-
self, owners_file_handler: OwnersFileHandler, mock_pull_request: Mock, tmp_path: Path
92-
) -> None:
93-
"""Test list_changed_files method using git diff."""
94-
# Set up mock PR SHAs
95-
mock_pull_request.base.sha = "base123abc"
96-
mock_pull_request.head.sha = "head456def"
90+
async def test_list_changed_files(self, owners_file_handler: OwnersFileHandler, tmp_path: Path) -> None:
91+
"""Test list_changed_files reads SHAs from GithubWebhook instance."""
92+
# SHAs are stored on the GithubWebhook instance during process()
93+
owners_file_handler.github_webhook.pr_base_sha = "base123abc"
94+
owners_file_handler.github_webhook.pr_head_sha = "head456def"
9795

9896
# Set up handler properties
9997
owners_file_handler.github_webhook.clone_repo_dir = str(tmp_path)
@@ -105,7 +103,7 @@ async def test_list_changed_files(
105103
) as mock_run_command:
106104
mock_run_command.return_value = (True, "file1.py\nfile2.py\n", "")
107105

108-
result = await owners_file_handler.list_changed_files(mock_pull_request)
106+
result = await owners_file_handler.list_changed_files()
109107

110108
# Verify result
111109
assert result == ["file1.py", "file2.py"]

0 commit comments

Comments
 (0)