Skip to content

Commit

Permalink
create fallback issue
Browse files Browse the repository at this point in the history
  • Loading branch information
Naoray committed Jan 16, 2025
1 parent 85b6d25 commit cd366d0
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 61 deletions.
76 changes: 46 additions & 30 deletions src/Issues/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;

class Handler extends AbstractProcessingHandler
{
private const DEFAULT_LABEL = 'github-issue-logger';
private PendingRequest $client;

/**
* @param string $repo The GitHub repository in "owner/repo" format
Expand All @@ -30,6 +33,7 @@ public function __construct(
$this->repo = $repo;
$this->token = $token;
$this->labels = array_unique(array_merge([self::DEFAULT_LABEL], $labels));
$this->client = Http::withToken($this->token)->baseUrl('https://api.github.com');
}

/**
Expand All @@ -38,19 +42,27 @@ public function __construct(
protected function write(LogRecord $record): void
{
if (! $record->formatted instanceof Formatted) {
throw new \RuntimeException('Record must be formatted with '.Formatted::class);
throw new \RuntimeException('Record must be formatted with ' . Formatted::class);
}

$formatted = $record->formatted;
$existingIssue = $this->findExistingIssue($record);

if ($existingIssue) {
$this->commentOnIssue($existingIssue['number'], $formatted);
try {
$existingIssue = $this->findExistingIssue($record);

return;
}
if ($existingIssue) {
$this->commentOnIssue($existingIssue['number'], $formatted);
return;
}

$this->createIssue($formatted);
} catch (RequestException $e) {
if ($e->response->serverError()) {
throw $e;
}

$this->createIssue($formatted);
$this->createFallbackIssue($formatted, $e->response->body());
}
}

/**
Expand All @@ -62,47 +74,51 @@ private function findExistingIssue(LogRecord $record): ?array
throw new \RuntimeException('Record is missing github_issue_signature in extra data. Make sure the DeduplicationHandler is configured correctly.');
}

$response = Http::withToken($this->token)
->get('https://api.github.com/search/issues', [
'q' => "repo:{$this->repo} is:issue is:open label:".self::DEFAULT_LABEL." \"Signature: {$record->extra['github_issue_signature']}\"",
]);

if ($response->failed()) {
throw new \RuntimeException('Failed to search GitHub issues: '.$response->body());
}

return $response->json('items.0', null);
return $this->client
->get('/search/issues', [
'q' => "repo:{$this->repo} is:issue is:open label:" . self::DEFAULT_LABEL . " \"Signature: {$record->extra['github_issue_signature']}\"",
])
->throw()
->json('items.0', null);
}

/**
* Add a comment to an existing issue
*/
private function commentOnIssue(int $issueNumber, Formatted $formatted): void
{
$response = Http::withToken($this->token)
->post("https://api.github.com/repos/{$this->repo}/issues/{$issueNumber}/comments", [
$this->client
->post("/repos/{$this->repo}/issues/{$issueNumber}/comments", [
'body' => $formatted->comment,
]);

if ($response->failed()) {
throw new \RuntimeException('Failed to comment on GitHub issue: '.$response->body());
}
])
->throw();
}

/**
* Create a new GitHub issue
*/
private function createIssue(Formatted $formatted): void
{
$response = Http::withToken($this->token)
->post("https://api.github.com/repos/{$this->repo}/issues", [
$this->client
->post("/repos/{$this->repo}/issues", [
'title' => $formatted->title,
'body' => $formatted->body,
'labels' => $this->labels,
]);
])
->throw();
}

if ($response->failed()) {
throw new \RuntimeException('Failed to create GitHub issue: '.$response->body());
}
/**
* Create a fallback issue when the main issue creation fails
*/
private function createFallbackIssue(Formatted $formatted, string $errorMessage): void
{
$this->client
->post("/repos/{$this->repo}/issues", [
'title' => '[GitHub Monolog Error] ' . $formatted->title,
'body' => "**Original Error Message:**\n{$formatted->body}\n\n**Integration Error:**\n{$errorMessage}",
'labels' => array_merge($this->labels, ['monolog-integration-error']),
])
->throw();
}
}
91 changes: 60 additions & 31 deletions tests/Issues/HandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Tests\Issues;

use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Monolog\Level;
use Monolog\LogRecord;
Expand Down Expand Up @@ -35,91 +37,118 @@ function createRecord(): LogRecord
);
}

test('it creates new github issue when no duplicate exists', function () {
$handler = createHandler();
$record = createRecord();
beforeEach(function () {
Http::preventStrayRequests();
});

test('it creates new github issue when no duplicate exists', function () {
Http::fake([
'github.com/search/issues*' => Http::response(['items' => []]),
'github.com/repos/test/repo/issues' => Http::response(['number' => 1]),
]);

$handler = createHandler();
$record = createRecord();

$handler->handle($record);

Http::assertSent(function ($request) {
return str_contains($request->url(), '/repos/test/repo/issues')
&& $request->method() === 'POST';
Http::assertSent(function (Request $request) {
return str($request->url())->endsWith('/repos/test/repo/issues');
});
});

test('it comments on existing github issue', function () {
$handler = createHandler();
$record = createRecord();

Http::fake([
'github.com/search/issues*' => Http::response(['items' => [['number' => 1]]]),
'github.com/repos/test/repo/issues/1/comments' => Http::response(['id' => 1]),
]);

$handler = createHandler();
$record = createRecord();

$handler->handle($record);

Http::assertSent(function ($request) {
return str_contains($request->url(), '/issues/1/comments')
&& $request->method() === 'POST';
return str($request->url())->endsWith('/issues/1/comments');
});
});

test('it includes signature in issue search', function () {
$handler = createHandler();
$record = createRecord();

Http::fake([
'github.com/search/issues*' => Http::response(['items' => []]),
'github.com/repos/test/repo/issues' => Http::response(['number' => 1]),
]);

$handler = createHandler();
$record = createRecord();

$handler->handle($record);

Http::assertSent(function ($request) {
return str_contains($request->url(), '/search/issues')
&& str_contains($request['q'], 'test-signature');
return str($request->url())->contains('/search/issues')
&& str_contains($request->data()['q'], 'test-signature');
});
});

test('it throws exception when issue search fails', function () {
$handler = createHandler();
$record = createRecord();

Http::fake([
'github.com/search/issues*' => Http::response(['error' => 'Failed'], 500),
]);

expect(fn () => $handler->handle($record))
->toThrow('Failed to search GitHub issues');
});

test('it throws exception when issue creation fails', function () {
$handler = createHandler();
$record = createRecord();

$handler->handle($record);
})->throws(RequestException::class, exceptionCode: 500);

test('it throws exception when issue creation fails', function () {
Http::fake([
'github.com/search/issues*' => Http::response(['items' => []]),
'github.com/repos/test/repo/issues' => Http::response(['error' => 'Failed'], 500),
]);

expect(fn () => $handler->handle($record))
->toThrow('Failed to create GitHub issue');
});

test('it throws exception when comment creation fails', function () {
$handler = createHandler();
$record = createRecord();

$handler->handle($record);
})->throws(RequestException::class, exceptionCode: 500);

test('it throws exception when comment creation fails', function () {
Http::fake([
'github.com/search/issues*' => Http::response(['items' => [['number' => 1]]]),
'github.com/repos/test/repo/issues/1/comments' => Http::response(['error' => 'Failed'], 500),
]);

expect(fn () => $handler->handle($record))
->toThrow('Failed to comment on GitHub issue');
$handler = createHandler();
$record = createRecord();

$handler->handle($record);
})->throws(RequestException::class, exceptionCode: 500);

test('it creates fallback issue when 4xx error occurs', function () {
$errorMessage = 'Validation failed for the issue';

Http::fake([
'github.com/search/issues*' => Http::response(['items' => []]),
'github.com/repos/test/repo/issues' => Http::sequence()
->push(['error' => $errorMessage], 422)
->push(['number' => 1]),
]);

$handler = createHandler();
$record = createRecord();

$handler->handle($record);

Http::assertSent(function ($request) {
return str($request->url())->endsWith('/repos/test/repo/issues')
&& !str_contains($request->data()['title'], '[GitHub Monolog Error]');
});

Http::assertSent(function ($request) use ($errorMessage) {
return str($request->url())->endsWith('/repos/test/repo/issues')
&& str_contains($request->data()['title'], '[GitHub Monolog Error]')
&& str_contains($request->data()['body'], $errorMessage)
&& in_array('monolog-integration-error', $request->data()['labels']);
});
});

0 comments on commit cd366d0

Please sign in to comment.