From 27df5ff13f3eb03e7e513ddb3802048914eef3ed Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 10:56:32 +0530 Subject: [PATCH 01/18] feat: add createCheckRun for Check Runs API Add createCheckRun() method to post a completed check run with a given conclusion (neutral, success, failure, etc.) using the GitHub Check Runs API, which supports richer conclusions than the legacy Commit Status API. --- src/VCS/Adapter/Git/GitHub.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index a06337ee..04cd8fed 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -872,6 +872,28 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); } + /** + * Creates a completed check run for a commit. + * conclusion can be one of: action_required, cancelled, failure, neutral, success, skipped, stale, timed_out + */ + public function createCheckRun(string $owner, string $repositoryName, string $headSha, string $name, string $conclusion, string $title, string $summary): void + { + $url = "/repos/$owner/$repositoryName/check-runs"; + + $body = [ + 'name' => $name, + 'head_sha' => $headSha, + 'status' => 'completed', + 'conclusion' => $conclusion, + 'output' => [ + 'title' => $title, + 'summary' => $summary, + ], + ]; + + $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + } + /** * Generates a clone command using app access token */ From 530dc9b152a72c0f5b99e8f7922f386d4ef57b1f Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 11:04:18 +0530 Subject: [PATCH 02/18] test: add testCreateCheckRun for GitHub Check Runs API --- tests/VCS/Adapter/GitHubTest.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index f929b3a5..63f35fc8 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -649,6 +649,33 @@ public function testUpdateCommitStatus(): void } } + public function testCreateCheckRun(): void + { + $repositoryName = 'test-create-check-run-' . \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']; + + // Should not throw + $this->vcsAdapter->createCheckRun( + static::$owner, + $repositoryName, + $commitHash, + 'ci/build', + 'neutral', + 'Deployment skipped', + 'Deployment skipped because the commit message contains a skip pattern.' + ); + + $this->assertTrue(true); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + public function testGenerateCloneCommand(): void { $repositoryName = 'test-clone-command-' . \uniqid(); From ef4d594ea984d48d5aaa37e58e418c937579e0f4 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 11:05:19 +0530 Subject: [PATCH 03/18] fix: add completed_at and remove stale from valid conclusions --- src/VCS/Adapter/Git/GitHub.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 04cd8fed..851da1ca 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -874,7 +874,7 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s /** * Creates a completed check run for a commit. - * conclusion can be one of: action_required, cancelled, failure, neutral, success, skipped, stale, timed_out + * conclusion can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out */ public function createCheckRun(string $owner, string $repositoryName, string $headSha, string $name, string $conclusion, string $title, string $summary): void { @@ -885,6 +885,7 @@ public function createCheckRun(string $owner, string $repositoryName, string $he 'head_sha' => $headSha, 'status' => 'completed', 'conclusion' => $conclusion, + 'completed_at' => date('Y-m-d\TH:i:s\Z'), 'output' => [ 'title' => $title, 'summary' => $summary, From 0c40bfa31fa02050d8c5b5d7d04ec7960a0eb6d8 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 11:09:03 +0530 Subject: [PATCH 04/18] fix: declare createCheckRun on base Adapter with not-implemented default --- src/VCS/Adapter.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index be9504de..5bfe3c6a 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -235,6 +235,15 @@ abstract public function listBranches(string $owner, string $repositoryName): ar */ abstract public function updateCommitStatus(string $repositoryName, string $SHA, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void; + /** + * Creates a completed check run for a commit. + * conclusion can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + */ + public function createCheckRun(string $owner, string $repositoryName, string $headSha, string $name, string $conclusion, string $title, string $summary): void + { + throw new \Exception('createCheckRun() is not implemented for ' . $this->getName()); + } + /** * Get repository tree * From 83b83d690fbd9f08c8b6e45bc5cb456ed45f5069 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 11:10:35 +0530 Subject: [PATCH 05/18] fix: use gmdate for UTC timestamp in createCheckRun --- src/VCS/Adapter/Git/GitHub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 851da1ca..f2dbc2bd 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -885,7 +885,7 @@ public function createCheckRun(string $owner, string $repositoryName, string $he 'head_sha' => $headSha, 'status' => 'completed', 'conclusion' => $conclusion, - 'completed_at' => date('Y-m-d\TH:i:s\Z'), + 'completed_at' => gmdate('Y-m-d\TH:i:s\Z'), 'output' => [ 'title' => $title, 'summary' => $summary, From f5fe27736dc234321e1bde7f230816588cfa9977 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 12:54:43 +0530 Subject: [PATCH 06/18] feat: expand createCheckRun params, add getCheckRun, improve test assertions --- src/VCS/Adapter.php | 34 +++++++++++++-- src/VCS/Adapter/Git/GitHub.php | 73 +++++++++++++++++++++++++++----- tests/VCS/Adapter/GitHubTest.php | 29 ++++++++----- 3 files changed, 111 insertions(+), 25 deletions(-) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index 5bfe3c6a..5355602d 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -236,14 +236,40 @@ abstract public function listBranches(string $owner, string $repositoryName): ar abstract public function updateCommitStatus(string $repositoryName, string $SHA, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void; /** - * Creates a completed check run for a commit. - * conclusion can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * Creates a check run for a commit. + * status can be one of: queued, in_progress, completed + * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * + * @return array */ - public function createCheckRun(string $owner, string $repositoryName, string $headSha, string $name, string $conclusion, string $title, string $summary): void - { + public function createCheckRun( + string $owner, + string $repositoryName, + string $headSha, + string $name, + string $status = 'queued', + string $conclusion = '', + string $title = '', + string $summary = '', + string $text = '', + string $detailsUrl = '', + string $externalId = '', + string $startedAt = '', + string $completedAt = '', + ): array { throw new \Exception('createCheckRun() is not implemented for ' . $this->getName()); } + /** + * Gets a check run by ID. + * + * @return array + */ + public function getCheckRun(string $owner, string $repositoryName, int $checkRunId): array + { + throw new \Exception('getCheckRun() is not implemented for ' . $this->getName()); + } + /** * Get repository tree * diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index f2dbc2bd..e9b1ed7d 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -873,26 +873,77 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s } /** - * Creates a completed check run for a commit. - * conclusion can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * Creates a check run for a commit. + * status can be one of: queued, in_progress, completed + * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * + * @return array */ - public function createCheckRun(string $owner, string $repositoryName, string $headSha, string $name, string $conclusion, string $title, string $summary): void - { + public function createCheckRun( + string $owner, + string $repositoryName, + string $headSha, + string $name, + string $status = 'queued', + string $conclusion = '', + string $title = '', + string $summary = '', + string $text = '', + string $detailsUrl = '', + string $externalId = '', + string $startedAt = '', + string $completedAt = '', + ): array { $url = "/repos/$owner/$repositoryName/check-runs"; $body = [ 'name' => $name, 'head_sha' => $headSha, - 'status' => 'completed', - 'conclusion' => $conclusion, - 'completed_at' => gmdate('Y-m-d\TH:i:s\Z'), - 'output' => [ + 'status' => $status, + ]; + + if (!empty($detailsUrl)) { + $body['details_url'] = $detailsUrl; + } + if (!empty($externalId)) { + $body['external_id'] = $externalId; + } + if (!empty($startedAt)) { + $body['started_at'] = $startedAt; + } + if (!empty($conclusion)) { + $body['conclusion'] = $conclusion; + } + if (!empty($completedAt)) { + $body['completed_at'] = $completedAt; + } + if (!empty($title) || !empty($summary)) { + $body['output'] = [ 'title' => $title, 'summary' => $summary, - ], - ]; + ]; + if (!empty($text)) { + $body['output']['text'] = $text; + } + } - $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + + return $response['body'] ?? []; + } + + /** + * Gets a check run by ID. + * + * @return array + */ + public function getCheckRun(string $owner, string $repositoryName, int $checkRunId): array + { + $url = "/repos/$owner/$repositoryName/check-runs/$checkRunId"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + return $response['body'] ?? []; } /** diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index 63f35fc8..497df030 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -659,18 +659,27 @@ public function testCreateCheckRun(): void $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $commitHash = $commit['commitHash']; - // Should not throw - $this->vcsAdapter->createCheckRun( - static::$owner, - $repositoryName, - $commitHash, - 'ci/build', - 'neutral', - 'Deployment skipped', - 'Deployment skipped because the commit message contains a skip pattern.' + $checkRun = $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash, + name: 'ci/build', + status: 'completed', + conclusion: 'neutral', + title: 'Deployment skipped', + summary: 'Deployment skipped because the commit message contains a skip pattern.', + completedAt: gmdate('Y-m-d\TH:i:s\Z'), ); - $this->assertTrue(true); + $this->assertArrayHasKey('id', $checkRun); + $this->assertEquals('completed', $checkRun['status']); + $this->assertEquals('neutral', $checkRun['conclusion']); + $this->assertEquals('ci/build', $checkRun['name']); + + $fetched = $this->vcsAdapter->getCheckRun(static::$owner, $repositoryName, $checkRun['id']); + $this->assertEquals($checkRun['id'], $fetched['id']); + $this->assertEquals('neutral', $fetched['conclusion']); + $this->assertEquals('completed', $fetched['status']); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } From d733ddfbd3198ed7b65ebdaa0423967819d2cc95 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 12:57:45 +0530 Subject: [PATCH 07/18] refactor: replace if-empty blocks with array_filter in createCheckRun --- src/VCS/Adapter/Git/GitHub.php | 42 ++++++++++++++-------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index e9b1ed7d..1007bb8b 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -896,35 +896,27 @@ public function createCheckRun( ): array { $url = "/repos/$owner/$repositoryName/check-runs"; - $body = [ - 'name' => $name, - 'head_sha' => $headSha, - 'status' => $status, - ]; + $body = array_merge( + [ + 'name' => $name, + 'head_sha' => $headSha, + 'status' => $status, + ], + array_filter([ + 'details_url' => $detailsUrl, + 'external_id' => $externalId, + 'started_at' => $startedAt, + 'conclusion' => $conclusion, + 'completed_at' => $completedAt, + ], fn ($value) => !empty($value)) + ); - if (!empty($detailsUrl)) { - $body['details_url'] = $detailsUrl; - } - if (!empty($externalId)) { - $body['external_id'] = $externalId; - } - if (!empty($startedAt)) { - $body['started_at'] = $startedAt; - } - if (!empty($conclusion)) { - $body['conclusion'] = $conclusion; - } - if (!empty($completedAt)) { - $body['completed_at'] = $completedAt; - } if (!empty($title) || !empty($summary)) { - $body['output'] = [ + $body['output'] = array_filter([ 'title' => $title, 'summary' => $summary, - ]; - if (!empty($text)) { - $body['output']['text'] = $text; - } + 'text' => $text, + ], fn ($value) => !empty($value)); } $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); From 7de3f137ee54d1b9fc44d8db3f5e745e64d451d5 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 12:59:40 +0530 Subject: [PATCH 08/18] feat: add annotations and images params to createCheckRun output --- src/VCS/Adapter.php | 4 ++++ src/VCS/Adapter/Git/GitHub.php | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index 5355602d..de254464 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -240,6 +240,8 @@ abstract public function updateCommitStatus(string $repositoryName, string $SHA, * status can be one of: queued, in_progress, completed * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out * + * @param array $annotations + * @param array $images * @return array */ public function createCheckRun( @@ -252,6 +254,8 @@ public function createCheckRun( string $title = '', string $summary = '', string $text = '', + array $annotations = [], + array $images = [], string $detailsUrl = '', string $externalId = '', string $startedAt = '', diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 1007bb8b..3a37b225 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -879,6 +879,10 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s * * @return array */ + /** + * @param array $annotations + * @param array $images + */ public function createCheckRun( string $owner, string $repositoryName, @@ -889,6 +893,8 @@ public function createCheckRun( string $title = '', string $summary = '', string $text = '', + array $annotations = [], + array $images = [], string $detailsUrl = '', string $externalId = '', string $startedAt = '', @@ -912,11 +918,20 @@ public function createCheckRun( ); if (!empty($title) || !empty($summary)) { - $body['output'] = array_filter([ + $output = array_filter([ 'title' => $title, 'summary' => $summary, 'text' => $text, ], fn ($value) => !empty($value)); + + if (!empty($annotations)) { + $output['annotations'] = $annotations; + } + if (!empty($images)) { + $output['images'] = $images; + } + + $body['output'] = $output; } $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); From bd9164dfbb6803741590dd130ae2524be0175d58 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 13:12:26 +0530 Subject: [PATCH 09/18] feat: add updateCheckRun method and test --- src/VCS/Adapter.php | 29 +++++++++++++++ src/VCS/Adapter/Git/GitHub.php | 60 ++++++++++++++++++++++++++++++++ tests/VCS/Adapter/GitHubTest.php | 41 ++++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index de254464..7cb8470f 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -274,6 +274,35 @@ public function getCheckRun(string $owner, string $repositoryName, int $checkRun throw new \Exception('getCheckRun() is not implemented for ' . $this->getName()); } + /** + * Updates an existing check run. + * status can be one of: queued, in_progress, completed + * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * + * @param array $annotations + * @param array $images + * @return array + */ + public function updateCheckRun( + string $owner, + string $repositoryName, + int $checkRunId, + string $name = '', + string $status = '', + string $conclusion = '', + string $title = '', + string $summary = '', + string $text = '', + array $annotations = [], + array $images = [], + string $detailsUrl = '', + string $externalId = '', + string $startedAt = '', + string $completedAt = '', + ): array { + throw new \Exception('updateCheckRun() is not implemented for ' . $this->getName()); + } + /** * Get repository tree * diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 3a37b225..11b18015 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -953,6 +953,66 @@ public function getCheckRun(string $owner, string $repositoryName, int $checkRun return $response['body'] ?? []; } + /** + * Updates an existing check run. + * status can be one of: queued, in_progress, completed + * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * + * @param array $annotations + * @param array $images + * @return array + */ + public function updateCheckRun( + string $owner, + string $repositoryName, + int $checkRunId, + string $name = '', + string $status = '', + string $conclusion = '', + string $title = '', + string $summary = '', + string $text = '', + array $annotations = [], + array $images = [], + string $detailsUrl = '', + string $externalId = '', + string $startedAt = '', + string $completedAt = '', + ): array { + $url = "/repos/$owner/$repositoryName/check-runs/$checkRunId"; + + $body = array_filter([ + 'name' => $name, + 'status' => $status, + 'details_url' => $detailsUrl, + 'external_id' => $externalId, + 'started_at' => $startedAt, + 'conclusion' => $conclusion, + 'completed_at' => $completedAt, + ], fn ($value) => !empty($value)); + + if (!empty($title) || !empty($summary)) { + $output = array_filter([ + 'title' => $title, + 'summary' => $summary, + 'text' => $text, + ], fn ($value) => !empty($value)); + + if (!empty($annotations)) { + $output['annotations'] = $annotations; + } + if (!empty($images)) { + $output['images'] = $images; + } + + $body['output'] = $output; + } + + $response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + + return $response['body'] ?? []; + } + /** * Generates a clone command using app access token */ diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index 497df030..55ccfc64 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -685,6 +685,47 @@ public function testCreateCheckRun(): void } } + public function testUpdateCheckRun(): void + { + $repositoryName = 'test-update-check-run-' . \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', + startedAt: gmdate('Y-m-d\TH:i:s\Z'), + ); + + $this->assertArrayHasKey('id', $checkRun); + $this->assertEquals('in_progress', $checkRun['status']); + + $updated = $this->vcsAdapter->updateCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + checkRunId: $checkRun['id'], + status: 'completed', + conclusion: 'neutral', + title: 'Deployment skipped', + summary: 'Deployment skipped because the branch does not match the configured branch triggers.', + completedAt: gmdate('Y-m-d\TH:i:s\Z'), + ); + + $this->assertEquals($checkRun['id'], $updated['id']); + $this->assertEquals('completed', $updated['status']); + $this->assertEquals('neutral', $updated['conclusion']); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + public function testGenerateCloneCommand(): void { $repositoryName = 'test-clone-command-' . \uniqid(); From 3f51e43ea2f8688a0ac9677ec83bdb06ae712a1d Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 13:18:14 +0530 Subject: [PATCH 10/18] refactor: extract buildCheckRunOutput helper, fix split docblock on createCheckRun --- src/VCS/Adapter/Git/GitHub.php | 65 ++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 11b18015..3984eb88 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -877,11 +877,9 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s * status can be one of: queued, in_progress, completed * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out * - * @return array - */ - /** * @param array $annotations * @param array $images + * @return array */ public function createCheckRun( string $owner, @@ -917,20 +915,8 @@ public function createCheckRun( ], fn ($value) => !empty($value)) ); - if (!empty($title) || !empty($summary)) { - $output = array_filter([ - 'title' => $title, - 'summary' => $summary, - 'text' => $text, - ], fn ($value) => !empty($value)); - - if (!empty($annotations)) { - $output['annotations'] = $annotations; - } - if (!empty($images)) { - $output['images'] = $images; - } - + $output = $this->buildCheckRunOutput($title, $summary, $text, $annotations, $images); + if (!empty($output)) { $body['output'] = $output; } @@ -991,20 +977,8 @@ public function updateCheckRun( 'completed_at' => $completedAt, ], fn ($value) => !empty($value)); - if (!empty($title) || !empty($summary)) { - $output = array_filter([ - 'title' => $title, - 'summary' => $summary, - 'text' => $text, - ], fn ($value) => !empty($value)); - - if (!empty($annotations)) { - $output['annotations'] = $annotations; - } - if (!empty($images)) { - $output['images'] = $images; - } - + $output = $this->buildCheckRunOutput($title, $summary, $text, $annotations, $images); + if (!empty($output)) { $body['output'] = $output; } @@ -1013,6 +987,35 @@ public function updateCheckRun( return $response['body'] ?? []; } + /** + * Builds the output block for a check run. + * + * @param array $annotations + * @param array $images + * @return array + */ + private function buildCheckRunOutput(string $title, string $summary, string $text, array $annotations, array $images): array + { + if (empty($title) && empty($summary)) { + return []; + } + + $output = array_filter([ + 'title' => $title, + 'summary' => $summary, + 'text' => $text, + ], fn ($value) => !empty($value)); + + if (!empty($annotations)) { + $output['annotations'] = $annotations; + } + if (!empty($images)) { + $output['images'] = $images; + } + + return $output; + } + /** * Generates a clone command using app access token */ From 2f5683fdb7367899829750b5b58e29cd57bde50e Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 13:24:38 +0530 Subject: [PATCH 11/18] refactor: inline output block, remove unnecessary helper --- src/VCS/Adapter/Git/GitHub.php | 49 +++++++++++----------------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 3984eb88..3f64c095 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -915,8 +915,14 @@ public function createCheckRun( ], fn ($value) => !empty($value)) ); - $output = $this->buildCheckRunOutput($title, $summary, $text, $annotations, $images); - if (!empty($output)) { + if (!empty($title) || !empty($summary)) { + $output = array_filter(['title' => $title, 'summary' => $summary, 'text' => $text], fn ($value) => !empty($value)); + if (!empty($annotations)) { + $output['annotations'] = $annotations; + } + if (!empty($images)) { + $output['images'] = $images; + } $body['output'] = $output; } @@ -977,8 +983,14 @@ public function updateCheckRun( 'completed_at' => $completedAt, ], fn ($value) => !empty($value)); - $output = $this->buildCheckRunOutput($title, $summary, $text, $annotations, $images); - if (!empty($output)) { + if (!empty($title) || !empty($summary)) { + $output = array_filter(['title' => $title, 'summary' => $summary, 'text' => $text], fn ($value) => !empty($value)); + if (!empty($annotations)) { + $output['annotations'] = $annotations; + } + if (!empty($images)) { + $output['images'] = $images; + } $body['output'] = $output; } @@ -987,35 +999,6 @@ public function updateCheckRun( return $response['body'] ?? []; } - /** - * Builds the output block for a check run. - * - * @param array $annotations - * @param array $images - * @return array - */ - private function buildCheckRunOutput(string $title, string $summary, string $text, array $annotations, array $images): array - { - if (empty($title) && empty($summary)) { - return []; - } - - $output = array_filter([ - 'title' => $title, - 'summary' => $summary, - 'text' => $text, - ], fn ($value) => !empty($value)); - - if (!empty($annotations)) { - $output['annotations'] = $annotations; - } - if (!empty($images)) { - $output['images'] = $images; - } - - return $output; - } - /** * Generates a clone command using app access token */ From 49dc8944cb8d9d7b92058657babdf4a29e29c152 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 13:25:57 +0530 Subject: [PATCH 12/18] fix: enforce conclusion implies completed status and completed_at; require both title and summary for output block --- src/VCS/Adapter/Git/GitHub.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 3f64c095..d4ca7620 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -900,6 +900,14 @@ public function createCheckRun( ): array { $url = "/repos/$owner/$repositoryName/check-runs"; + // Conclusion requires status=completed; auto-set completed_at if not provided. + if (!empty($conclusion)) { + $status = 'completed'; + if (empty($completedAt)) { + $completedAt = gmdate('Y-m-d\TH:i:s\Z'); + } + } + $body = array_merge( [ 'name' => $name, @@ -915,7 +923,8 @@ public function createCheckRun( ], fn ($value) => !empty($value)) ); - if (!empty($title) || !empty($summary)) { + // Output requires both title and summary. + if (!empty($title) && !empty($summary)) { $output = array_filter(['title' => $title, 'summary' => $summary, 'text' => $text], fn ($value) => !empty($value)); if (!empty($annotations)) { $output['annotations'] = $annotations; @@ -973,6 +982,14 @@ public function updateCheckRun( ): array { $url = "/repos/$owner/$repositoryName/check-runs/$checkRunId"; + // Conclusion requires status=completed; auto-set completed_at if not provided. + if (!empty($conclusion)) { + $status = 'completed'; + if (empty($completedAt)) { + $completedAt = gmdate('Y-m-d\TH:i:s\Z'); + } + } + $body = array_filter([ 'name' => $name, 'status' => $status, @@ -983,7 +1000,8 @@ public function updateCheckRun( 'completed_at' => $completedAt, ], fn ($value) => !empty($value)); - if (!empty($title) || !empty($summary)) { + // Output requires both title and summary. + if (!empty($title) && !empty($summary)) { $output = array_filter(['title' => $title, 'summary' => $summary, 'text' => $text], fn ($value) => !empty($value)); if (!empty($annotations)) { $output['annotations'] = $annotations; From ccb0e8ea9d734f90937bc5ac3588b1cdfce2d8cd Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Tue, 26 May 2026 13:29:33 +0530 Subject: [PATCH 13/18] refactor: createCheckRun for initial status only, conclusion via updateCheckRun; add actions param --- src/VCS/Adapter.php | 10 ++++++---- src/VCS/Adapter/Git/GitHub.php | 27 +++++++++++++-------------- tests/VCS/Adapter/GitHubTest.php | 13 ++++--------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index 7cb8470f..c14a2236 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -237,11 +237,12 @@ abstract public function updateCommitStatus(string $repositoryName, string $SHA, /** * Creates a check run for a commit. - * status can be one of: queued, in_progress, completed - * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * status can be one of: queued, in_progress + * Use updateCheckRun() to set conclusion and mark the run as completed. * * @param array $annotations * @param array $images + * @param array $actions * @return array */ public function createCheckRun( @@ -250,16 +251,15 @@ public function createCheckRun( string $headSha, string $name, string $status = 'queued', - string $conclusion = '', string $title = '', string $summary = '', string $text = '', array $annotations = [], array $images = [], + array $actions = [], string $detailsUrl = '', string $externalId = '', string $startedAt = '', - string $completedAt = '', ): array { throw new \Exception('createCheckRun() is not implemented for ' . $this->getName()); } @@ -281,6 +281,7 @@ public function getCheckRun(string $owner, string $repositoryName, int $checkRun * * @param array $annotations * @param array $images + * @param array $actions * @return array */ public function updateCheckRun( @@ -295,6 +296,7 @@ public function updateCheckRun( string $text = '', array $annotations = [], array $images = [], + array $actions = [], string $detailsUrl = '', string $externalId = '', string $startedAt = '', diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index d4ca7620..565eea13 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -874,11 +874,12 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s /** * Creates a check run for a commit. - * status can be one of: queued, in_progress, completed - * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out + * status can be one of: queued, in_progress + * Use updateCheckRun() to set conclusion and mark the run as completed. * * @param array $annotations * @param array $images + * @param array $actions * @return array */ public function createCheckRun( @@ -887,27 +888,18 @@ public function createCheckRun( string $headSha, string $name, string $status = 'queued', - string $conclusion = '', string $title = '', string $summary = '', string $text = '', array $annotations = [], array $images = [], + array $actions = [], string $detailsUrl = '', string $externalId = '', string $startedAt = '', - string $completedAt = '', ): array { $url = "/repos/$owner/$repositoryName/check-runs"; - // Conclusion requires status=completed; auto-set completed_at if not provided. - if (!empty($conclusion)) { - $status = 'completed'; - if (empty($completedAt)) { - $completedAt = gmdate('Y-m-d\TH:i:s\Z'); - } - } - $body = array_merge( [ 'name' => $name, @@ -918,8 +910,6 @@ public function createCheckRun( 'details_url' => $detailsUrl, 'external_id' => $externalId, 'started_at' => $startedAt, - 'conclusion' => $conclusion, - 'completed_at' => $completedAt, ], fn ($value) => !empty($value)) ); @@ -935,6 +925,10 @@ public function createCheckRun( $body['output'] = $output; } + if (!empty($actions)) { + $body['actions'] = $actions; + } + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); return $response['body'] ?? []; @@ -975,6 +969,7 @@ public function updateCheckRun( string $text = '', array $annotations = [], array $images = [], + array $actions = [], string $detailsUrl = '', string $externalId = '', string $startedAt = '', @@ -1012,6 +1007,10 @@ public function updateCheckRun( $body['output'] = $output; } + if (!empty($actions)) { + $body['actions'] = $actions; + } + $response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "Bearer $this->accessToken"], $body); return $response['body'] ?? []; diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index 55ccfc64..894d8059 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -664,22 +664,17 @@ public function testCreateCheckRun(): void repositoryName: $repositoryName, headSha: $commitHash, name: 'ci/build', - status: 'completed', - conclusion: 'neutral', - title: 'Deployment skipped', - summary: 'Deployment skipped because the commit message contains a skip pattern.', - completedAt: gmdate('Y-m-d\TH:i:s\Z'), + status: 'in_progress', + startedAt: gmdate('Y-m-d\TH:i:s\Z'), ); $this->assertArrayHasKey('id', $checkRun); - $this->assertEquals('completed', $checkRun['status']); - $this->assertEquals('neutral', $checkRun['conclusion']); + $this->assertEquals('in_progress', $checkRun['status']); $this->assertEquals('ci/build', $checkRun['name']); $fetched = $this->vcsAdapter->getCheckRun(static::$owner, $repositoryName, $checkRun['id']); $this->assertEquals($checkRun['id'], $fetched['id']); - $this->assertEquals('neutral', $fetched['conclusion']); - $this->assertEquals('completed', $fetched['status']); + $this->assertEquals('in_progress', $fetched['status']); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } From 460cd88783f0a4c6a160a956ddd4387277acacaa Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 28 May 2026 13:25:52 +0530 Subject: [PATCH 14/18] feat: add conclusion/completedAt to createCheckRun; expand tests Add conclusion and completedAt params to createCheckRun to support one-shot completed runs; expand test assertions to cover all response fields; add failure tests (invalid repo, invalid ID) and multi-run tests (two runs on same commit, same name on different commits). --- src/VCS/Adapter/Git/GitHub.php | 16 +++- tests/VCS/Adapter/GitHubTest.php | 154 ++++++++++++++++++++++++++++++- 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 565eea13..6cf71382 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -874,8 +874,8 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s /** * Creates a check run for a commit. - * status can be one of: queued, in_progress - * Use updateCheckRun() to set conclusion and mark the run as completed. + * status can be one of: queued, in_progress, completed + * conclusion (required when status=completed) can be one of: action_required, cancelled, failure, neutral, success, skipped, timed_out * * @param array $annotations * @param array $images @@ -888,6 +888,7 @@ public function createCheckRun( string $headSha, string $name, string $status = 'queued', + string $conclusion = '', string $title = '', string $summary = '', string $text = '', @@ -897,9 +898,18 @@ public function createCheckRun( string $detailsUrl = '', string $externalId = '', string $startedAt = '', + string $completedAt = '', ): array { $url = "/repos/$owner/$repositoryName/check-runs"; + // Conclusion requires status=completed; auto-set completed_at if not provided. + if (!empty($conclusion)) { + $status = 'completed'; + if (empty($completedAt)) { + $completedAt = gmdate('Y-m-d\TH:i:s\Z'); + } + } + $body = array_merge( [ 'name' => $name, @@ -907,6 +917,8 @@ public function createCheckRun( 'status' => $status, ], array_filter([ + 'conclusion' => $conclusion, + 'completed_at' => $completedAt, 'details_url' => $detailsUrl, 'external_id' => $externalId, 'started_at' => $startedAt, diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index 894d8059..1f5d23c9 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -669,12 +669,164 @@ public function testCreateCheckRun(): void ); $this->assertArrayHasKey('id', $checkRun); - $this->assertEquals('in_progress', $checkRun['status']); + $this->assertIsInt($checkRun['id']); $this->assertEquals('ci/build', $checkRun['name']); + $this->assertEquals('in_progress', $checkRun['status']); + $this->assertNull($checkRun['conclusion']); + $this->assertEquals($commitHash, $checkRun['head_sha']); + $this->assertNotEmpty($checkRun['url']); + $this->assertNotEmpty($checkRun['html_url']); + $this->assertNotEmpty($checkRun['started_at']); + $this->assertNull($checkRun['completed_at']); $fetched = $this->vcsAdapter->getCheckRun(static::$owner, $repositoryName, $checkRun['id']); $this->assertEquals($checkRun['id'], $fetched['id']); + $this->assertEquals('ci/build', $fetched['name']); $this->assertEquals('in_progress', $fetched['status']); + $this->assertNull($fetched['conclusion']); + $this->assertEquals($commitHash, $fetched['head_sha']); + $this->assertNotEmpty($fetched['url']); + $this->assertNotEmpty($fetched['html_url']); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testCreateCheckRunWithInvalidRepository(): void + { + $this->expectException(\Exception::class); + $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: 'non-existing-repository-' . \uniqid(), + headSha: 'a' . str_repeat('0', 39), + name: 'ci/build', + ); + } + + public function testGetCheckRunWithInvalidId(): void + { + $repositoryName = 'test-get-check-run-invalid-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->expectException(\Exception::class); + $this->vcsAdapter->getCheckRun(static::$owner, $repositoryName, 999999999); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testCreateTwoCheckRunsOnSameCommit(): void + { + $repositoryName = 'test-two-check-runs-same-commit-' . \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->assertArrayHasKey('id', $first); + $this->assertArrayHasKey('id', $second); + $this->assertNotEquals($first['id'], $second['id']); + $this->assertEquals($commitHash, $first['head_sha']); + $this->assertEquals($commitHash, $second['head_sha']); + $this->assertEquals('ci/build', $first['name']); + $this->assertEquals('ci/build', $second['name']); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testCreateCheckRunsWithSameNameOnDifferentCommits(): void + { + $repositoryName = 'test-check-runs-different-commits-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $commit1 = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash1 = $commit1['commitHash']; + + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'second.md', '# Second'); + $commit2 = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); + $commitHash2 = $commit2['commitHash']; + + $first = $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash1, + name: 'ci/build', + status: 'in_progress', + ); + + $second = $this->vcsAdapter->createCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + headSha: $commitHash2, + name: 'ci/build', + status: 'in_progress', + ); + + $this->assertArrayHasKey('id', $first); + $this->assertArrayHasKey('id', $second); + $this->assertNotEquals($first['id'], $second['id']); + $this->assertEquals($commitHash1, $first['head_sha']); + $this->assertEquals($commitHash2, $second['head_sha']); + $this->assertEquals('ci/build', $first['name']); + $this->assertEquals('ci/build', $second['name']); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testCreateCheckRunCompleted(): void + { + $repositoryName = 'test-create-check-run-completed-' . \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', + conclusion: 'success', + title: 'Build passed', + summary: 'All checks passed successfully.', + ); + + $this->assertArrayHasKey('id', $checkRun); + $this->assertIsInt($checkRun['id']); + $this->assertEquals('ci/build', $checkRun['name']); + $this->assertEquals('completed', $checkRun['status']); + $this->assertEquals('success', $checkRun['conclusion']); + $this->assertEquals($commitHash, $checkRun['head_sha']); + $this->assertNotEmpty($checkRun['url']); + $this->assertNotEmpty($checkRun['html_url']); + $this->assertNotEmpty($checkRun['completed_at']); + $this->assertEquals('Build passed', $checkRun['output']['title']); + $this->assertEquals('All checks passed successfully.', $checkRun['output']['summary']); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } From 21015e961a274b62509246934f66b939071b647c Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 28 May 2026 13:29:52 +0530 Subject: [PATCH 15/18] fix: sync createCheckRun signature in base Adapter with GitHub implementation --- src/VCS/Adapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index c14a2236..1d4ca02b 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -251,6 +251,7 @@ public function createCheckRun( string $headSha, string $name, string $status = 'queued', + string $conclusion = '', string $title = '', string $summary = '', string $text = '', @@ -260,6 +261,7 @@ public function createCheckRun( string $detailsUrl = '', string $externalId = '', string $startedAt = '', + string $completedAt = '', ): array { throw new \Exception('createCheckRun() is not implemented for ' . $this->getName()); } From 6ba62a1d3a5abab24670e8528525ebdde5863d56 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 28 May 2026 13:33:58 +0530 Subject: [PATCH 16/18] fix: throw exception on HTTP errors in createCheckRun and getCheckRun --- src/VCS/Adapter/Git/GitHub.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 6cf71382..e50e34ea 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -943,6 +943,11 @@ public function createCheckRun( $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + $responseHeadersStatusCode = $response['headers']['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to create check run: HTTP $responseHeadersStatusCode"); + } + return $response['body'] ?? []; } @@ -957,6 +962,11 @@ public function getCheckRun(string $owner, string $repositoryName, int $checkRun $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + $responseHeadersStatusCode = $response['headers']['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to get check run $checkRunId: HTTP $responseHeadersStatusCode"); + } + return $response['body'] ?? []; } From 29e943b1cdc9f1d715b0d398a607f00568728207 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 28 May 2026 13:43:26 +0530 Subject: [PATCH 17/18] fix: throw on HTTP errors in updateCheckRun; add failure tests for updateCheckRun --- src/VCS/Adapter/Git/GitHub.php | 5 +++ tests/VCS/Adapter/GitHubTest.php | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index e50e34ea..7b3101b9 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -1035,6 +1035,11 @@ public function updateCheckRun( $response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "Bearer $this->accessToken"], $body); + $responseHeadersStatusCode = $response['headers']['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to update check run $checkRunId: HTTP $responseHeadersStatusCode"); + } + return $response['body'] ?? []; } diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index 1f5d23c9..f1c4b2fc 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -873,6 +873,65 @@ public function testUpdateCheckRun(): void } } + public function testUpdateCheckRunWithInvalidRepository(): void + { + $this->expectException(\Exception::class); + $this->vcsAdapter->updateCheckRun( + owner: static::$owner, + repositoryName: 'non-existing-repository-' . \uniqid(), + checkRunId: 999999999, + conclusion: 'success', + ); + } + + public function testUpdateCheckRunWithInvalidId(): void + { + $repositoryName = 'test-update-check-run-invalid-' . \uniqid(); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + + try { + $this->expectException(\Exception::class); + $this->vcsAdapter->updateCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + checkRunId: 999999999, + conclusion: 'success', + ); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + + public function testUpdateCheckRunWithMissingConclusion(): void + { + $repositoryName = 'test-update-check-run-no-conclusion-' . \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', + ); + + $this->expectException(\Exception::class); + $this->vcsAdapter->updateCheckRun( + owner: static::$owner, + repositoryName: $repositoryName, + checkRunId: $checkRun['id'], + status: 'completed', + ); + } finally { + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); + } + } + public function testGenerateCloneCommand(): void { $repositoryName = 'test-clone-command-' . \uniqid(); From ef78fe2cde95f018316e8281c4ae8bb83437cd89 Mon Sep 17 00:00:00 2001 From: harsh mahajan Date: Thu, 28 May 2026 13:44:10 +0530 Subject: [PATCH 18/18] fix: guard against status=completed without conclusion in createCheckRun and updateCheckRun --- src/VCS/Adapter/Git/GitHub.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 7b3101b9..1af30388 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -902,6 +902,10 @@ public function createCheckRun( ): array { $url = "/repos/$owner/$repositoryName/check-runs"; + if ($status === 'completed' && empty($conclusion)) { + throw new Exception("conclusion is required when status is 'completed'"); + } + // Conclusion requires status=completed; auto-set completed_at if not provided. if (!empty($conclusion)) { $status = 'completed'; @@ -999,6 +1003,10 @@ public function updateCheckRun( ): array { $url = "/repos/$owner/$repositoryName/check-runs/$checkRunId"; + if ($status === 'completed' && empty($conclusion)) { + throw new Exception("conclusion is required when status is 'completed'"); + } + // Conclusion requires status=completed; auto-set completed_at if not provided. if (!empty($conclusion)) { $status = 'completed';