Skip to content
Docs / Packages / Laravel Github

Laravel Github

Laravel GitHub Documentation

Documentation for the upcoming v1.0.4 release, following v1.0.3.

Contents


Introduction

Laravel GitHub is a Laravel package for connecting your application to a GitHub organisation through a GitHub App installation. It provides a straightforward way to browse repositories, list open pull requests, search pull requests and retrieve individual pull request details without implementing GitHub App authentication yourself.

The package handles installation token generation, renewal and response caching, allowing your application to focus on how repository and pull request information is presented and used.

1use Egough\LaravelGitHub\Facades\GitHub;
2 
3// Browse repositories accessible to the installation.
4$repositories = GitHub::repositories();
5 
6// List open pull requests, including draft and review-request metadata.
7$openPulls = GitHub::openPullRequests()
8 ->repository('acme/backend')
9 ->get();
10 
11// Search by title, including closed and merged pull requests.
12$results = GitHub::pullRequests()
13 ->repository('acme/backend')
14 ->search('checkout')
15 ->withClosed()
16 ->page(1)
17 ->get();
18 
19// Retrieve an individual pull request.
20$pull = GitHub::pullRequest('acme/backend', 42);

Existing direct calls remain supported:

1$results = GitHub::pullRequests('acme/backend', 'checkout', false, 1);
2$openPulls = GitHub::openPullRequests('acme/backend', 1);

The direct search above excludes closed PRs; add ->withClosed() to a fluent search when you want to include closed and merged PRs.


Changes in v1.0.4

Version v1.0.4 adds the following changes to v1.0.3:

  • Immutable fluent queries are available through pullRequests() and openPullRequests(), with named setters and an explicit get() execution method.
  • openPullRequests() lists open pull requests with draft flags, requested-review status, creation/update timestamps and pagination.
  • Package internals are organised into authentication, resource, HTTP transport, exception and cache-key classes. GitHubClient remains the public entry point.
  • Fixed exception messages are owned by named factory methods on their exception classes.
  • Exception classes now live under Egough\LaravelGitHub\Exceptions. The previous Egough\LaravelGitHub\GitHubUnavailable and Egough\LaravelGitHub\RepositoryUnavailable names are deprecated from v1.0.4, but remain supported through Composer-loaded aliases throughout the remaining v1.x releases.

Existing client construction, direct facade calls, named arguments, response shapes, configuration and caching behaviour remain supported. Chaining is additive in v1.0.4; applications do not need to migrate existing calls.

Breaking change planned for v2.0.0: src/Exceptions/aliases.php and its Composer autoload registration will be removed. Applications must update their exception imports to Egough\LaravelGitHub\Exceptions\GitHubUnavailable and Egough\LaravelGitHub\Exceptions\RepositoryUnavailable before upgrading. See Deprecated Exception Names and v2.0.0 Migration.


When To Use This Package

Use Laravel GitHub when your application needs read-only access to an organisation's repositories and pull requests, particularly when integrating GitHub information into an existing business workflow.

Common use cases:

  • Linking GitHub repositories to internal projects
  • Associating pull requests with support tickets or development tasks
  • Displaying pull request titles and current states
  • Building repository selectors and pull request search interfaces
  • Surfacing development activity in an internal application

When not to use this package:

Laravel GitHub currently supports one configured organisation and one GitHub App installation. It does not provide personal access token authentication, user OAuth, multiple connections or webhook handling.

It is also a read-only integration: creating repositories, modifying pull requests and merging code are outside its scope. Your application remains responsible for its own routes, authorization, user interface and storage of any selected repository or pull request links.


Installation

Install the package via Composer:

1composer require egough/laravel-github

Laravel automatically discovers the package's service provider, so no manual provider registration is required.

Publishing Configuration

Optionally publish the configuration file:

1php artisan vendor:publish --tag=github-config

This creates config/github.php, where you can adjust the GitHub connection and caching settings.

No migrations are required.


GitHub App Setup

Laravel GitHub authenticates through a GitHub App installed on the organisation whose repositories you want to access. This requires a GitHub App ID, installation ID and private key.

Creating and Installing the App

  1. Create a GitHub App and grant it Repository metadata: read and Pull requests: read permissions.
  2. Install the app on your GitHub organisation, selecting the repositories it should be allowed to access.
  3. Generate a private key for the app.
  4. Store the private key outside your source repository and publicly accessible web directories, in a location readable by your PHP process.

The package does not require a webhook endpoint or OAuth callback for this integration.

Connecting Your Laravel Application

Add the following settings to your application's .env file:

1GITHUB_ORGANISATION=acme
2GITHUB_APP_ID=123456
3GITHUB_INSTALLATION_ID=789012
4GITHUB_PRIVATE_KEY_PATH=/absolute/path/to/github-app.pem
Variable Description
GITHUB_ORGANISATION The GitHub organisation whose repositories will be browsed.
GITHUB_APP_ID The GitHub App ID, not the OAuth client ID.
GITHUB_INSTALLATION_ID The ID of the app's installation on the organisation.
GITHUB_PRIVATE_KEY_PATH Absolute path to the GitHub App private key file.

Keep the private key secure and do not commit it to version control.

Checking Configuration

Use the facade to check whether the required connection settings are present:

1use Egough\LaravelGitHub\Facades\GitHub;
2 
3$configured = GitHub::configured();

configured() checks for configuration values only. It does not verify that the private key is valid, that GitHub is reachable or that the installation has permission to access a repository.


Configuration

After publishing, the configuration file will be available at config/github.php.

1return [
2 'organisation' => env('GITHUB_ORGANISATION'),
3 'app_id' => env('GITHUB_APP_ID'),
4 'installation_id' => env('GITHUB_INSTALLATION_ID'),
5 'private_key_path' => env('GITHUB_PRIVATE_KEY_PATH'),
6 'user_agent' => 'egough-laravel-github',
7 'api_version' => '2026-03-10',
8 'cache' => [
9 'repositories' => 300,
10 'pull_requests' => 60,
11 ],
12];

Options

organisation

The GitHub organisation associated with the app installation. Repository access is restricted to this configured organisation.

app_id

The identifier of your GitHub App, used during application authentication.

installation_id

The installation identifier used to request an installation access token.

private_key_path

An absolute filesystem path to the app's private key. The PHP process must have permission to read this file.

user_agent

The User-Agent value sent to GitHub. Defaults to egough-laravel-github.

api_version

The GitHub API version sent with requests. The repository currently specifies 2026-03-10.

cache.repositories

How long repository lists are cached, in seconds. Defaults to 300 (five minutes).

cache.pull_requests

How long pull request searches, individual pull request details and open pull request pages are cached, in seconds. Defaults to 60 (one minute).


Repositories

Use the GitHub facade to retrieve repositories accessible through the configured GitHub App installation.

1use Egough\LaravelGitHub\Facades\GitHub;
2 
3$repositories = GitHub::repositories();

The method returns repositories belonging to the configured organisation, sorted by full name:

1[
2 [
3 'full_name' => 'acme/backend',
4 'url' => 'https://github.com/acme/backend',
5 ],
6 [
7 'full_name' => 'acme/frontend',
8 'url' => 'https://github.com/acme/frontend',
9 ],
10]
Field Description
full_name Repository identifier in owner/repository format.
url The repository's GitHub URL.

Repository pagination is handled automatically, so you do not need to request each page yourself.

Private repositories are included when the GitHub App installation has access to them. Repositories outside the configured organisation or outside the installation's granted access are not available through the package.


Pull Requests

The package provides methods for listing open pull requests, searching pull requests within an accessible repository and retrieving a specific pull request by number.

Fluent Query API

From v1.0.4, calling pullRequests() or openPullRequests() without a repository returns a new query builder. Calling either method with a repository executes immediately and returns the existing result array.

1$results = GitHub::pullRequests()
2 ->repository('acme/backend')
3 ->search('checkout')
4 ->withClosed()
5 ->page(1)
6 ->get();
7 
8$openPulls = GitHub::openPullRequests()
9 ->repository('acme/backend')
10 ->page(1)
11 ->get();

Builder creation and setters do not send HTTP requests. get() validates the query, then retrieves one page through the same resources, access checks, authentication and cache used by direct calls. Equivalent fluent and direct calls share cache entries.

Search queries support:

Method Purpose and Default
repository(string $repository) Set the repository in owner/repository format. Required before get().
search(string $search) Set the title search or exact PR number. Defaults to an empty search.
withClosed(bool $include = true) Include closed and merged PRs. Closed PRs are excluded unless enabled; withClosed(false) excludes them again.
page(int $page) Set the search page, from 1 to 34. Defaults to 1.
get() Execute and return the existing data, next_page and incomplete result array.

Open-list queries support repository(), page() and get(). Their page must be positive, with no search-specific upper limit. They return the existing data and next_page result array, including draft and review-request metadata. Drafts remain included, and these builders do not expose search() or withClosed().

All setters are optional except that a repository must be supplied before execution. get() throws MissingRepository for a missing or blank repository, and InvalidPage for a page outside the fluent query's bounds. Both exceptions live under Egough\LaravelGitHub\Exceptions and extend InvalidArgumentException. Validation happens before authentication or HTTP requests; building an invalid query alone does not throw.

The fluent search page bounds do not change the existing direct search method's input-validation behaviour. Applications should continue validating input passed to direct calls.

You can also initialise options with named arguments while omitting the repository:

1$query = GitHub::pullRequests(search: 'checkout', includeClosed: true, page: 2);
2$results = $query->repository('acme/backend')->get();
3 
4$query = GitHub::openPullRequests(page: 2);
5$openPulls = $query->repository('acme/backend')->get();

The same fluent API is available on an injected GitHubClient. Individual PR lookups through pullRequest() and repository browsing through repositories() retain their existing APIs.

Reusing Queries

Queries are immutable: each setter returns a new query without modifying its source. A base query can safely be reused for different searches or pages:

1$base = GitHub::pullRequests()->repository('acme/backend');
2$checkout = $base->search('checkout');
3 
4$openCheckout = $checkout->get();
5$allCheckout = $checkout->withClosed()->get();
6$nextCheckoutPage = $checkout->page(2)->get();
7$otherSearch = $base->search('shipping')->get();

$base still has an empty search, and $checkout still excludes closed PRs and uses page 1. Each fresh facade call also starts with the defaults.

Capture the returned builder when updating a variable:

1$query = GitHub::openPullRequests()->repository('acme/backend');
2$query = $query->page(2);
3$results = $query->get();

If a setter is repeated in a chain, its last value takes effect. get() does not advance the page or modify the builder; use the returned next_page value to construct the next query.

Listing Open Pull Requests

Available from v1.0.4, openPullRequests() lists open pull requests in an accessible repository:

1$results = GitHub::openPullRequests(
2 repository: 'acme/backend',
3 page: 1,
4);

The equivalent fluent call is:

1$results = GitHub::openPullRequests()
2 ->repository('acme/backend')
3 ->get();

The page argument is optional and defaults to 1. It must be greater than zero; invalid pages throw Egough\LaravelGitHub\Exceptions\InvalidPage, which extends PHP's InvalidArgumentException.

1[
2 'data' => [
3 [
4 'url' => 'https://github.com/acme/backend/pull/42',
5 'number' => 42,
6 'title' => 'Fix checkout',
7 'state' => 'open',
8 'draft' => false,
9 'waiting_for_review' => true,
10 'created_at' => '2026-10-20T12:00:00Z',
11 'updated_at' => '2026-10-21T12:00:00Z',
12 ],
13 ],
14 'next_page' => null,
15]
Field Description
draft Whether the pull request is a draft. Drafts are included in the results.
waiting_for_review Whether GitHub currently lists any requested reviewers or requested teams. This does not mean that the PR has no approvals.
created_at Creation timestamp returned by GitHub.
updated_at Last update timestamp returned by GitHub.

Results are ordered by least recently updated first, with up to 100 pull requests per page. This method uses the repository pull request listing endpoint rather than search, so the search API's 1,000-result limit does not apply. Its response has data and next_page; it does not contain the search method's incomplete field.

Pass next_page into the next call until it is null:

1$page = 1;
2 
3do {
4 $results = GitHub::openPullRequests('acme/backend', $page);
5 
6 foreach ($results['data'] as $pull) {
7 if ($pull['draft']) {
8 continue;
9 }
10 
11 // Present or process this non-draft pull request.
12 }
13 
14 $page = $results['next_page'];
15} while ($page !== null);

Filtering drafts, associating pull requests with projects or tickets, and deciding which users should see them remain application responsibilities. pullRequests() and pullRequest() retain their existing response fields; the additional metadata above is provided by openPullRequests().

Searching Pull Requests

Use pullRequests() to search by title:

1$results = GitHub::pullRequests(
2 repository: 'acme/backend',
3 search: 'checkout',
4 includeClosed: false,
5 page: 1,
6);
Argument Description
repository Repository full name in owner/repository format.
search Title search term. An empty search retrieves recently updated pull requests.
includeClosed Set to true to include closed and merged pull requests.
page The search results page to retrieve.

The result contains the matching pull requests and pagination information:

1[
2 'data' => [
3 [
4 'url' => 'https://github.com/acme/backend/pull/42',
5 'number' => 42,
6 'title' => 'Fix checkout',
7 'state' => 'open',
8 ],
9 ],
10 'next_page' => null,
11 'incomplete' => false,
12]

data contains the matching pull requests. Each item includes the URL, pull request number, title and state.

next_page identifies the next results page, or is null when no further page is available. Pass this value as the page argument in a subsequent call to retrieve more results.

incomplete indicates that GitHub did not return a complete search result. When this is true, consider asking users to narrow their search.

Results are ordered by most recently updated, with 30 results per page. GitHub search exposes a maximum of 1,000 results, so applications should validate page input and avoid unbounded searches. The fluent search builder enforces pages 1 to 34 at get().

Finding a Pull Request by Number

The search method also supports exact-number lookups. Pass either a number such as 42 or a hash-prefixed value such as #42 as the search term:

1$results = GitHub::pullRequests(
2 repository: 'acme/backend',
3 search: '#42',
4 includeClosed: true,
5 page: 1,
6);

The includeClosed filter still applies to exact-number searches. If you want to find a merged or closed pull request, set it to true.

Retrieving a Pull Request

Use pullRequest() when you already know the pull request number:

1$pull = GitHub::pullRequest('acme/backend', 42);

A matching pull request is returned with its URL, number, title and state:

1[
2 'url' => 'https://github.com/acme/backend/pull/42',
3 'number' => 42,
4 'title' => 'Fix checkout',
5 'state' => 'open',
6]

The method returns null when the individual pull request cannot be found. Repository access is still restricted to the configured organisation and installation.

Understanding Pull Request States

The package returns the following state values:

State Description
open The pull request is currently open.
closed The pull request was closed without a confirmed merge.
merged The pull request was merged, when GitHub provides merge information in its response.

This allows you to display a simplified state in your application without interpreting GitHub's raw response yourself.


Dependency Injection

In addition to the facade, you can inject the GitHub client into your controllers or services through Laravel's service container.

1use Egough\LaravelGitHub\GitHubClient;
2 
3public function __construct(
4 private GitHubClient $github,
5) {}

You can then call the same methods through the injected client:

1$repositories = $this->github->repositories();

The facade and dependency-injected client resolve the same shared container instance. Choose whichever approach best fits your application's architecture.


Authentication and Caching

Laravel GitHub handles GitHub App authentication automatically. It signs a JSON Web Token (JWT) using your app's private key and exchanges that token for an installation access token.

Installation tokens are cached until one minute before their expiry. If GitHub returns a 401 response, the package refreshes the token and retries the request once.

Response Caching

The package uses Laravel's default cache store:

Cached Item Default Duration
Repository lists 300 seconds (five minutes)
Pull request searches 60 seconds (one minute)
Individual pull request details 60 seconds (one minute)
Open pull request pages 60 seconds (one minute)
Installation access tokens Until one minute before expiry

Cache keys are scoped to the configured connection settings. Because installation tokens are sensitive credentials, use a protected cache backend with appropriate access controls.

Request Timeouts

Requests use a three-second connection timeout and a ten-second overall timeout. These limits help prevent an unavailable GitHub API from indefinitely blocking your application.


Package Structure

From v1.0.4, the source is organised by responsibility:

1src/
2├── Auth/
3│ └── InstallationTokenManager.php
4├── Exceptions/
5│ ├── GitHubUnavailable.php
6│ ├── InvalidPage.php
7│ ├── MissingRepository.php
8│ ├── RepositoryUnavailable.php
9│ └── aliases.php
10├── Facades/
11│ └── GitHub.php
12├── Http/
13│ ├── AuthenticatedTransport.php
14│ └── RequestFactory.php
15├── Queries/
16│ ├── OpenPullRequestQuery.php
17│ └── PullRequestQuery.php
18├── Resources/
19│ ├── PullRequests.php
20│ └── Repositories.php
21├── Support/
22│ └── CacheKey.php
23├── GitHubClient.php
24└── GitHubServiceProvider.php

InstallationTokenManager handles JWT signing, installation tokens, expiry and invalidation. The immutable query builders collect fluent options and delegate execution to the resource classes, which handle repository access and pull request queries. The HTTP classes share request configuration, translate connection errors and handle authenticated requests with one token refresh after a 401. CacheKey keeps connection-scoped cache keys consistent across these responsibilities.

Applications should continue to use the facade or inject GitHubClient; they do not need to construct these internal collaborators. The existing new GitHubClient($config) constructor remains supported.


Error Handling

The package raises exceptions for connection, authentication and repository access problems, leaving your application responsible for how errors are presented to users.

1use Egough\LaravelGitHub\Facades\GitHub;
2use Egough\LaravelGitHub\Exceptions\GitHubUnavailable;
3use Egough\LaravelGitHub\Exceptions\RepositoryUnavailable;
4 
5try {
6 $pull = GitHub::pullRequest('acme/backend', 42);
7} catch (RepositoryUnavailable $exception) {
8 // The repository is outside the configured organisation
9 // or the GitHub App installation's access.
10} catch (GitHubUnavailable $exception) {
11 // Configuration, authentication or an upstream request failed.
12}
Exception Meaning
RepositoryUnavailable The requested repository is not available to the configured organisation and installation.
GitHubUnavailable A configuration, authentication or upstream GitHub request failed.
InvalidPage An open-list page is below 1, or a fluent search page is outside 1 to 34. Extends InvalidArgumentException.
MissingRepository A fluent query was executed without a non-blank repository. Extends InvalidArgumentException.

A missing individual pull request returns null rather than throwing RepositoryUnavailable.

Upstream response bodies are not included in exception messages.

Exception Factory Methods

Exception classes provide named static factories so each fixed error message is defined in one place:

1use Egough\LaravelGitHub\Exceptions\GitHubUnavailable;
2use Egough\LaravelGitHub\Exceptions\InvalidPage;
3use Egough\LaravelGitHub\Exceptions\MissingRepository;
4use Egough\LaravelGitHub\Exceptions\RepositoryUnavailable;
5 
6throw GitHubUnavailable::notConfigured();
7throw RepositoryUnavailable::notAccessible();
8throw InvalidPage::mustBePositive();
9throw MissingRepository::required();

Each factory returns an exception instance; calling it alone does not throw. Package error messages retain their existing wording.

Factory Condition
GitHubUnavailable::notConfigured() Required connection settings are missing.
GitHubUnavailable::privateKeyUnavailable() The private key file cannot be read.
GitHubUnavailable::privateKeyInvalid() The private key cannot be parsed.
GitHubUnavailable::signingFailed() Signing the GitHub App JWT failed.
GitHubUnavailable::authenticationFailed() The installation token request failed or returned an invalid response.
GitHubUnavailable::connectionFailed() An HTTP request could not be sent successfully.
GitHubUnavailable::upstreamUnavailable() An authenticated GitHub request returned an unsuccessful response.
RepositoryUnavailable::notAccessible() The repository is outside the configured organisation or installation access.
InvalidPage::mustBePositive() An open-list or fluent search page is below 1.
InvalidPage::searchLimitExceeded() A fluent search page exceeds 34.
MissingRepository::required() A fluent query was executed without a non-blank repository.

The standard exception constructors remain available if your application needs to construct an exception with its own message.

Deprecated Exception Names and v2.0.0 Migration

The old root-level exception names are deprecated starting in v1.0.4:

Deprecated import Replacement import
Egough\LaravelGitHub\GitHubUnavailable Egough\LaravelGitHub\Exceptions\GitHubUnavailable
Egough\LaravelGitHub\RepositoryUnavailable Egough\LaravelGitHub\Exceptions\RepositoryUnavailable

In v1.0.4 and the remaining v1.x releases, Composer automatically loads src/Exceptions/aliases.php. It uses class_alias() so each old name and its replacement refer to the same exception class. Existing imports, exception construction and catch blocks continue to work during that period. The deprecation is documented; the aliases do not emit runtime deprecation warnings.

In v2.0.0, aliases.php and its Composer autoload registration will be removed. The old exception names will no longer be provided. Update imports before upgrading:

1-use Egough\LaravelGitHub\GitHubUnavailable;
2-use Egough\LaravelGitHub\RepositoryUnavailable;
3+use Egough\LaravelGitHub\Exceptions\GitHubUnavailable;
4+use Egough\LaravelGitHub\Exceptions\RepositoryUnavailable;

Catch blocks using these imported short names can stay as they are:

1try {
2 $pull = GitHub::pullRequest('acme/backend', 42);
3} catch (RepositoryUnavailable $exception) {
4 // Repository access failed.
5} catch (GitHubUnavailable $exception) {
6 // Configuration, authentication or an upstream request failed.
7}

Also update fully qualified exception references, PHPDoc types and exception class strings in tests or configuration that still use the deprecated paths. Existing catch (\InvalidArgumentException $exception) handlers catch both InvalidPage and MissingRepository; neither is a legacy alias.

Validating Application Input

Your application remains responsible for validating input, authorizing users and throttling any endpoints that expose package functionality.

For browsing endpoints, validate repository identifiers in owner/repository format, positive pull request numbers, bounded search strings and search page numbers from 1 to 34.


Testing

The package includes tests that fake HTTP requests and use an in-memory cache. GitHub credentials, network access and an external database are not required to run the suite after installing dependencies.

Running the Tests

Install development dependencies:

1composer install

Run the package test suite:

1composer test

Or run PHPUnit directly:

1vendor/bin/phpunit

Testing Your Own Application

When testing an application that uses Laravel GitHub, you can use Laravel's HTTP testing utilities to fake GitHub API responses:

1use Illuminate\Support\Facades\Http;
2 
3Http::fake([
4 'api.github.com/*' => Http::response([
5 'repositories' => [],
6 ]),
7]);
8 
9Http::preventStrayRequests();

This is an illustrative HTTP fake; configure the mocked response shape for the specific GitHub endpoint exercised by your test. You may also need to fake authentication endpoints and clear cached values between test cases, depending on the code path being tested.


Local Development

To use a local checkout of Laravel GitHub in another Laravel application, add a Composer path repository to that application's composer.json:

1{
2 "repositories": [
3 {
4 "type": "path",
5 "url": "../packages/laravel-github",
6 "options": {
7 "symlink": true,
8 "reference": "config",
9 "versions": {
10 "egough/laravel-github": "1.0.4"
11 }
12 }
13 }
14 ]
15}

Then require the package:

1composer require egough/laravel-github:^1.0.4

Adjust the path to match your local directory layout. With symlinking enabled, PHP source changes in the package checkout become available to the consuming application immediately.

The local version override is a concrete version (1.0.4), not a version constraint. The reference: config setting lets Composer detect changes to the package manifest even before a commit is made. Rerun composer update egough/laravel-github when package metadata or dependencies change so the consuming application registers changes such as the exception alias autoload file. Before using the application in CI or production, replace the local path dependency with a published release or an accessible Git repository.


Requirements

Requirement Version
PHP 8.2+
Laravel 10, 11, 12 or 13 (subject to each version's PHP requirements)
PHP extension OpenSSL

The package is released under the MIT license.