Framework-neutral GitHub App authentication and GitHub Actions workflow dispatch for PHP 8.3 and newer.
The package creates short-lived GitHub App JWTs, exchanges them for installation access tokens, and dispatches repository workflows through injected PSR-18 and PSR-17 implementations. It has no Laravel, Symfony, Slim, Laminas, dotenv, HTTP-client implementation, service-container, cache, or queue dependency.
License status: this repository is currently marked proprietary while permission to select an open-source license is resolved. Do not publish or redistribute it as open source until a license is approved and added. See License.
- PHP 8.3 or newer
- OpenSSL and JSON PHP extensions
- A PSR-18 HTTP client
- PSR-17 request and stream factories
After the package is made available through Packagist or another Composer repository:
composer require fnet/fnet-github-dispatch-workflowInstall a PSR-18 client and PSR-17 implementation appropriate for your application. They are intentionally not selected by this package. For example, an application may use Guzzle plus its PSR-7 factories, Symfony's PSR-18 client plus PSR-17 factories, or any other compliant implementations.
The package has not been published to Packagist.
<?php
declare(strict_types=1);
use Fnet\GitHubDispatchWorkflow\Configuration;
use Fnet\GitHubDispatchWorkflow\GitHubAppClient;
$configuration = new Configuration(
appId: '12345',
installationId: '67890',
privateKey: $privateKeyPem,
userAgent: 'acme-deployer/1.0',
);
$github = new GitHubAppClient(
configuration: $configuration,
httpClient: $psr18Client,
requestFactory: $psr17RequestFactory,
streamFactory: $psr17StreamFactory,
logger: $psr3Logger, // Optional; defaults to NullLogger.
clock: $psrClock, // Optional; primarily useful for deterministic testing.
);Configure connection and request timeouts on the supplied PSR-18 implementation. The package does not retry requests: callers can make an informed retry decision, especially because workflow dispatch is not safely repeatable without application-level coordination.
The consuming application is responsible for loading environment variables. This package does not read .env files and does not depend on a dotenv package.
use Fnet\GitHubDispatchWorkflow\Configuration;
use Fnet\GitHubDispatchWorkflow\Workflow;
$configuration = Configuration::fromEnvironment([
// A non-null explicit value overrides the corresponding environment value.
'user_agent' => 'acme-deployer/1.0',
]);
$workflow = Workflow::fromEnvironment([
'ref' => 'release',
]);Environment lookup order is getenv(), $_ENV, then $_SERVER. Explicit non-null values take precedence. An explicit empty string is invalid; it does not silently fall back.
$jwt = $github->createJwt();The RS256 JWT uses the configured app ID as iss, backdates iat by 60 seconds for clock skew, and expires ten minutes after that issued-at value. Private keys can be supplied as:
- Inline PEM
- Inline PEM containing literal
\nseparators - Strict base64-encoded PEM
- A readable PEM file path
The package reads file-backed keys but never writes private keys to disk.
$installationToken = $github->createInstallationToken();
$token = $installationToken->value();
$expiresAt = $installationToken->expiresAt();Treat value() as a secret. The package requests a new installation token for each call and does not cache it.
use Fnet\GitHubDispatchWorkflow\Workflow;
$workflow = new Workflow(
owner: 'acme',
repository: 'website',
workflow: 'deploy.yml', // A workflow filename or string identifier.
ref: 'main',
);
$github->dispatchWorkflow($workflow, [
'app' => 'acme-portal',
'environment' => 'production',
]);The method obtains a fresh installation token and sends {"ref": ..., "inputs": ...}. Inputs may contain strings, integers, floats, booleans, or null. Only GitHub's HTTP 204 response is accepted as a successful dispatch. The package does not inject application-specific inputs.
| Programmatic field | Environment variable | Required | Notes |
|---|---|---|---|
Configuration::$appId |
GITHUB_APP_ID |
Yes | Used as the JWT issuer. |
Configuration::$installationId |
GITHUB_APP_INSTALLATION_ID |
Yes | Installation receiving access tokens. |
Configuration::$privateKey |
GITHUB_APP_PRIVATE_KEY |
Yes | Inline, escaped-newline, base64 PEM, or PEM path. |
Configuration::$userAgent |
GITHUB_USER_AGENT |
No | Defaults to FNET-GitHub-Dispatch-Workflow; CR/LF is rejected. |
Workflow::$owner |
GITHUB_WORKFLOW_OWNER |
Yes for fromEnvironment() |
Repository owner. |
Workflow::$repository |
GITHUB_WORKFLOW_REPO |
Yes for fromEnvironment() |
Repository name. |
Workflow::$workflow |
GITHUB_WORKFLOW_ID |
Yes for fromEnvironment() |
Workflow filename or string identifier. |
Workflow::$ref |
GITHUB_WORKFLOW_REF |
Yes for fromEnvironment() |
Branch, tag, or commit-compatible Git ref accepted by GitHub. |
use Fnet\GitHubDispatchWorkflow\Exception\AuthenticationException;
use Fnet\GitHubDispatchWorkflow\Exception\ConfigurationException;
use Fnet\GitHubDispatchWorkflow\Exception\GitHubAppException;
use Fnet\GitHubDispatchWorkflow\Exception\JwtException;
use Fnet\GitHubDispatchWorkflow\Exception\TransportException;
use Fnet\GitHubDispatchWorkflow\Exception\WorkflowDispatchException;
try {
$github->dispatchWorkflow($workflow, ['app' => 'acme']);
} catch (ConfigurationException|JwtException $exception) {
// Correct local configuration or key material.
} catch (AuthenticationException|WorkflowDispatchException $exception) {
$status = $exception->statusCode();
$requestId = $exception->requestId();
// Log only sanitized context.
} catch (TransportException $exception) {
// The original PSR-18 ClientExceptionInterface is getPrevious().
} catch (GitHubAppException $exception) {
// Catch every package exception when category-specific handling is unnecessary.
}Package exception messages exclude private keys, JWTs, installation tokens, authorization headers, and raw response bodies. Lower-level transport and parsing exceptions remain available through getPrevious().
Supplying a PSR-3 logger is optional. Logs contain operation metadata such as installation ID, repository, workflow, ref, status, request ID, and expiration. They never contain private keys, JWTs, tokens, or Authorization headers.
- Store GitHub App keys in your platform's secret manager or protected filesystem.
- Restrict file permissions for path-based keys.
- Never print or persist values returned by
InstallationToken::value(). - Give the GitHub App only the repository and Actions permissions it needs.
- Configure TLS verification and conservative timeouts on the PSR-18 client.
- Do not retry workflow dispatch blindly; coordinate retries in the consuming application.
See SECURITY.md for vulnerability reporting and supported versions.
The package needs the same five objects in every environment: configuration, a PSR-18 client, PSR-17 request and stream factories, and optionally a PSR-3 logger.
- Laravel: bind
GitHubAppClientin an application service provider using values from a Laravel config file. Pass the framework's PSR-compatible transport/factories or installed adapters. Keep queueing, deduplication, and domain rules in Laravel jobs/services. - Symfony: define
ConfigurationandGitHubAppClientas services; inject Symfony's PSR-18 client, compatible PSR-17 factories,logger, and optionallyclock. - Slim: register the objects in the application's chosen container or construct them in bootstrap code, then inject the client into handlers.
- Laminas: provide factories for
ConfigurationandGitHubAppClient, reusing PSR HTTP implementations already configured by the application. - Plain PHP: construct the objects directly as shown above. No container is required.
No framework adapter or auto-discovery hook is needed.
composer install
composer test
composer analyse
composer check-style
composer validate-package
composer checkTests use generated ephemeral RSA keys and in-memory PSR-18 responses. They never call GitHub.
The package has no release tag. The documented public classes are the intended stable surface for a future 0.x release. Internal classes may change without notice until a license and first release are approved. Semantic Versioning should be used after release.
No open-source license has been selected. The source application is proprietary, so this repository is marked proprietary in Composer metadata and remains untagged pending explicit authorization and license selection. Add an approved LICENSE file and update composer.json, this README, and SECURITY.md before public distribution.