Skip to content

Latest commit

 

History

History
216 lines (156 loc) · 9.38 KB

File metadata and controls

216 lines (156 loc) · 9.38 KB

FNET-GitHub Dispatch Workflow

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.

Requirements

  • PHP 8.3 or newer
  • OpenSSL and JSON PHP extensions
  • A PSR-18 HTTP client
  • PSR-17 request and stream factories

Installation

After the package is made available through Packagist or another Composer repository:

composer require fnet/fnet-github-dispatch-workflow

Install 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.

Create a client

<?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.

Environment fallback

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.

GitHub App JWT

$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 \n separators
  • Strict base64-encoded PEM
  • A readable PEM file path

The package reads file-backed keys but never writes private keys to disk.

Installation access token

$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.

Workflow dispatch

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.

Configuration reference

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.

Error handling

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().

Logging and security

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.

Framework integration

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 GitHubAppClient in 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 Configuration and GitHubAppClient as services; inject Symfony's PSR-18 client, compatible PSR-17 factories, logger, and optionally clock.
  • 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 Configuration and GitHubAppClient, 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.

Development

composer install
composer test
composer analyse
composer check-style
composer validate-package
composer check

Tests use generated ephemeral RSA keys and in-memory PSR-18 responses. They never call GitHub.

Stability and versioning

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.

Design documentation

License

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.