Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/VCS/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ public function createCheckRun(
throw new \Exception('createCheckRun() is not implemented for ' . $this->getName());
}

/**
* Finds the most recent check run on a commit by name.
* Returns the check run ID, 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());
}

/**
* Gets a check run by ID.
*
Expand Down
24 changes: 24 additions & 0 deletions src/VCS/Adapter/Git/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,30 @@ public function createCheckRun(
return $response['body'] ?? [];
}

/**
* Finds the most recent check run on a commit by name.
* Returns the check run ID, or 0 if none found.
*/
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.
*
Expand Down
167 changes: 167 additions & 0 deletions tests/VCS/Adapter/GitHubTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
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.

P2 The assertGreaterThan before calling getCheckRunByName is used to validate that GitHub assigned a higher ID to the second run, but it's also a load-bearing pre-condition of the test: if GitHub ever returns equal IDs (e.g. the same check run was deduplicated), the assertion fails before the actual behavior is exercised. Swapping the argument order also silently inverts the comparison — assertGreaterThan($expected, $actual) means "assert that $expected > $actual", so assertGreaterThan($first['id'], $second['id']) asserts that $first['id'] > $second['id'], which is the opposite of what the comment says. Here the call is correct as written — but consider using assertLessThan for clarity: assertLessThan($second['id'], $first['id']) reads more naturally as "first is less than second".

Suggested change
$this->assertGreaterThan($first['id'], $second['id']);
$this->assertLessThan($second['id'], $first['id']);

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


$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();
Expand Down
Loading