From 6f9878b8f6a339cd221f812d48cc1dc3cc84d6ae Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 8 Jun 2026 16:37:02 +0530 Subject: [PATCH 1/3] feat: add getCheckRunByName --- src/VCS/Adapter.php | 5 + src/VCS/Adapter/Git/GitHub.php | 20 ++++ tests/VCS/Adapter/GitHubTest.php | 168 +++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index 1d4ca02b..a717eb28 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -266,6 +266,11 @@ public function createCheckRun( throw new \Exception('createCheckRun() is not implemented for ' . $this->getName()); } + public function getCheckRunByName(string $owner, string $repositoryName, string $ref, string $checkName): int + { + throw new \Exception('getCheckRunByName() is not implemented for ' . $this->getName()); + } + /** * Gets a check run by ID. * diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 1af30388..0f912ecc 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -955,6 +955,26 @@ public function createCheckRun( return $response['body'] ?? []; } + public function getCheckRunByName(string $owner, string $repositoryName, string $ref, string $checkName): int + { + $url = "/repos/$owner/$repositoryName/commits/$ref/check-runs"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [ + 'check_name' => $checkName, + 'filter' => 'latest', + 'per_page' => 1, + ]); + + $responseHeadersStatusCode = $response['headers']['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + return 0; + } + + $runs = $response['body']['check_runs'] ?? []; + + return (int) ($runs[0]['id'] ?? 0); + } + /** * Gets a check run by ID. * diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index f1c4b2fc..ed1cf3cf 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -932,6 +932,173 @@ public function testUpdateCheckRunWithMissingConclusion(): void } } + public function testGetCheckRunByName(): void + { + $repositoryName = 'test-get-check-run-by-name-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $checkRun = $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash, + name: 'ci/build', + status: 'in_progress', + ); + + $foundId = $this->vcsAdapter->getCheckRunByName( + static::$owner, + $repositoryName, + $commitHash, + 'ci/build' + ); + + $this->assertEquals($checkRun['id'], $foundId); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testGetCheckRunByNameNoMatchReturnsZero(): void + { + // Verifies the check_name filter is actually applied: + // a run with a different name must not be returned. + $repositoryName = 'test-get-check-run-by-name-nomatch-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash, + name: 'ci/build', + status: 'in_progress', + ); + + $foundId = $this->vcsAdapter->getCheckRunByName( + static::$owner, + $repositoryName, + $commitHash, + 'ci/lint' // different name + ); + + $this->assertEquals(0, $foundId); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testGetCheckRunByNameInvalidRepositoryReturnsZero(): void + { + // Non-existent repo must return 0, not throw — callers rely on this + // for graceful fallback to the legacy commit status API. + $foundId = $this->vcsAdapter->getCheckRunByName( + static::$owner, + 'non-existing-repository-' . \uniqid(), + str_repeat('a', 40), + 'ci/build' + ); + + $this->assertEquals(0, $foundId); + } + + public function testGetCheckRunByNameReturnsMostRecent(): void + { + // When a commit has multiple runs with the same name (e.g. retries), + // the most recently created one must be returned. + $repositoryName = 'test-get-check-run-by-name-recent-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $first = $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash, + name: 'ci/build', + status: 'in_progress', + ); + + $second = $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash, + name: 'ci/build', + status: 'in_progress', + ); + + $this->assertGreaterThan($first['id'], $second['id']); + + $foundId = $this->vcsAdapter->getCheckRunByName( + static::$owner, + $repositoryName, + $commitHash, + 'ci/build' + ); + + $this->assertEquals($second['id'], $foundId); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testGetCheckRunByNameThenUpdate(): void + { + // End-to-end: create as in_progress, look up by name, update to completed. + // This is the exact workflow the method was designed for — no stored ID needed. + $repositoryName = 'test-get-check-run-by-name-update-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash = $commit['commitHash']; + + $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash, + name: 'ci/build', + status: 'in_progress', + ); + + $checkRunId = $this->vcsAdapter->getCheckRunByName( + static::$owner, + $repositoryName, + $commitHash, + 'ci/build' + ); + + $this->assertGreaterThan(0, $checkRunId); + + $updated = $this->vcsAdapter->updateCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + checkRunId: $checkRunId, + conclusion: 'success', + title: 'Build succeeded.', + summary: 'All steps passed.', + ); + + $this->assertEquals($checkRunId, $updated['id']); + $this->assertEquals('completed', $updated['status']); + $this->assertEquals('success', $updated['conclusion']); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + public function testGenerateCloneCommand(): void { $repositoryName = 'test-clone-command-' . \uniqid(); @@ -1112,4 +1279,5 @@ public function testUpdateComment(): void { $this->markTestSkipped('Requires existing PR — createPullRequest not implemented in GitHub adapter'); } + } From 68841d97c6fdbbb963e20e22e1274ba3cf8d5a5d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 8 Jun 2026 16:42:23 +0530 Subject: [PATCH 2/3] fix: return 0 only on 404, throw on other HTTP errors --- src/VCS/Adapter/Git/GitHub.php | 5 ++++- tests/VCS/Adapter/GitHubTest.php | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 0f912ecc..32257317 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -966,9 +966,12 @@ public function getCheckRunByName(string $owner, string $repositoryName, string ]); $responseHeadersStatusCode = $response['headers']['status-code'] ?? 0; - if ($responseHeadersStatusCode >= 400) { + if ($responseHeadersStatusCode === 404) { return 0; } + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to get check run by name: HTTP $responseHeadersStatusCode"); + } $runs = $response['body']['check_runs'] ?? []; diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index ed1cf3cf..bd728e95 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -996,10 +996,8 @@ public function testGetCheckRunByNameNoMatchReturnsZero(): void } } - public function testGetCheckRunByNameInvalidRepositoryReturnsZero(): void + public function testGetCheckRunByNameNotFoundRepositoryReturnsZero(): void { - // Non-existent repo must return 0, not throw — callers rely on this - // for graceful fallback to the legacy commit status API. $foundId = $this->vcsAdapter->getCheckRunByName( static::$owner, 'non-existing-repository-' . \uniqid(), From 52c8fa628390c14f3ca7df6819e65f1050da295d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Mon, 8 Jun 2026 16:51:01 +0530 Subject: [PATCH 3/3] fix: use filter=all + max(id) instead of filter=latest to reliably return the most recently created run --- src/VCS/Adapter.php | 3 +++ src/VCS/Adapter/Git/GitHub.php | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index a717eb28..e986e09f 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -266,6 +266,9 @@ public function createCheckRun( throw new \Exception('createCheckRun() is not implemented for ' . $this->getName()); } + /** + * Returns the ID of the most recently created check run with the given name on a commit ref, or 0 if none found. + */ public function getCheckRunByName(string $owner, string $repositoryName, string $ref, string $checkName): int { throw new \Exception('getCheckRunByName() is not implemented for ' . $this->getName()); diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 32257317..59af56f2 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -961,8 +961,8 @@ public function getCheckRunByName(string $owner, string $repositoryName, string $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [ 'check_name' => $checkName, - 'filter' => 'latest', - 'per_page' => 1, + 'filter' => 'all', + 'per_page' => 100, ]); $responseHeadersStatusCode = $response['headers']['status-code'] ?? 0; @@ -974,8 +974,12 @@ public function getCheckRunByName(string $owner, string $repositoryName, string } $runs = $response['body']['check_runs'] ?? []; + if (empty($runs)) { + return 0; + } - return (int) ($runs[0]['id'] ?? 0); + // GitHub IDs are monotonically increasing; the highest ID is the most recently created run. + return (int) max(array_column($runs, 'id')); } /**