---
title: "GitHub Actions — Git for Laravel | Roundly"
description: "Dispatch a workflow and get the run it started, poll its status, jobs and steps, query runs by branch, event or status, and cancel them."
url: https://roundly-consulting.com/open-source/docs/git-for-laravel/github-actions
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Git for Laravel](https://roundly-consulting.com/open-source/docs/git-for-laravel.md)

# GitHub Actions

repo($path)->actions() drives GitHub Actions for one repository: start a workflow on a ref, get back the run GitHub started, follow it — its status, its jobs and each job’s steps — and cancel it. GitLab and Bitbucket throw FeatureNotSupportedException; their pipelines aren’t mapped.

```php
use RoundlyConsulting\Git\Enums\WorkflowStatus;
use RoundlyConsulting\Git\Facades\Git;

$actions = Git::github($credential)->repo('acme/infra')->actions();

$dispatched = $actions->dispatch('deploy.yml', 'refs/heads/main', ['request_id' => $id]);
$run = $actions->run($dispatched->runId);              // WorkflowRun
$run->isActive();                                       // poll until completed

$actions->runs('deploy.yml')->branch('main')->event('workflow_dispatch')
    ->status(WorkflowStatus::InProgress)->createdAfter($since)->get();   // Page<WorkflowRun>

$actions->jobs($run->id)->allAttempts()->lazy();        // WorkflowJob, with ->steps
$actions->jobs($run->id)->perPage(1)->get()->total;    // did the run start any job?
$actions->cancel($run->id);                             // true = accepted, false = already done
```

| Handle method | Returns |
| --- | --- |
| `dispatch($workflow, $ref, $inputs = [])` | DispatchedWorkflow — the runId, apiUrl and url of the run GitHub started, plus dispatchedAt. |
| `runs(?$workflow)` | WorkflowRunQuery — newest first; no workflow means every workflow of the repository. |
| `run($id)` | WorkflowRun — the run as it is now. |
| `jobs($runId)` | WorkflowJobQuery — the latest attempt unless allAttempts() or attempt($n). |
| `cancel($runId)` | bool — true when GitHub accepted the cancel, false when the run had already finished (409). |

Every workflow and run id is checked before a request — an id lands in a URL, and ../7 or 7/jobs would address another resource. A workflow is its file name (deploy.yml — one segment ending .yml or .yaml) or its numeric id, and a run id is digits only; anything else throws OutOfScopeException. dispatch() and cancel() need a credential.

## Dispatching a workflow

- $ref is sent as given — main and refs/heads/main both work. $inputs are the workflow’s declared inputs, each a string, number or boolean; a positional key or a nested or null value throws InvalidArgumentException. How many inputs a workflow may take is GitHub’s 422, not a client-side cap.
- On github.com and GHE.com, GitHub answers with the run it started, so runId, apiUrl and url are set. GitHub Enterprise Server hasn’t documented that answer, so there all three are null — find the run as shown below.
- dispatchedAt is the local clock, read before the request leaves, so a query from it cannot miss the run.
- A dispatch is never retried: a lost answer followed by a retry would start a second build.

## Finding a run without runId

On GitHub Enterprise Server, or to adopt a run started elsewhere, put a marker in the workflow’s run-name — run-name: deploy \[${{ inputs.request\_id }}\] — and look for it among the runs created since the dispatch:

```php
use RoundlyConsulting\Git\Dto\WorkflowRun;

// .github/workflows/deploy.yml — run-name: deploy [${{ inputs.request_id }}]
$candidates = $actions->runs('deploy.yml')
    ->event('workflow_dispatch')
    ->branch('main')
    ->createdAfter($dispatched->dispatchedAt->copy()->subMinute())
    ->get();

$run = $candidates->collect()->first(fn (WorkflowRun $run) => str_contains($run->displayTitle, "[{$id}]"));
```

The matching policy — which marker, the oldest or the newest run, extra checks on path or headBranch — stays with you; no extra API call is made for it.

## Querying runs and jobs

- runs() filters with branch(), event(), status(), actor() (a login), headSha(), createdAfter() and createdBefore(). status() takes a WorkflowStatus or a WorkflowConclusion — GitHub accepts both in one filter — and throws InvalidArgumentException for Unknown.
- createdAfter() and createdBefore() compare in UTC to the second and never move your Carbon instance. excludePullRequests() only leaves each run’s pull\_requests out of GitHub’s answer — it filters nothing.
- The page’s total is GitHub’s total\_count. GitHub stops a filtered run search at 1,000 results, so lazy() ends there too — a total above what you read says so.
- jobs() reads the run’s latest attempt; allAttempts() reads every attempt and attempt($n) one of them (1 is the first run). Asking for both, or for an attempt below 1, throws InvalidArgumentException.

## Polling and cancelling

- run($id) goes through the conditional cache, so with GIT\_CACHE\_ENABLED=true an unchanged run comes back as a 304, which GitHub doesn’t count against the primary rate limit.
- isActive() is true for anything but completed — an Unknown status included — so a poll never stops on a state it cannot read. A status or conclusion GitHub adds later reads as Unknown, which never counts as completed or successful.
- runStartedAt is the creation time while a run is queued or waiting; trust it only once the run is in progress.
- GitHub cancels asynchronously: true means the cancel was accepted, and the outcome is the next run() (wasCancelled()). false means the run had already finished. Force-cancel is not offered.

## Result objects

| Object | Fields |
| --- | --- |
| `WorkflowRun` | id, workflowId, name, displayTitle, status, conclusion, event, path, headBranch, headSha, headRepository, runNumber, runAttempt, actor, triggeringActor, url, createdAt, updatedAt, runStartedAt — plus isCompleted(), isActive(), succeeded(), wasCancelled() |
| `WorkflowJob` | id, runId, name, status, conclusion, headSha, runAttempt, workflowName, headBranch, startedAt, completedAt, runnerName, labels, url, steps (list of JobStep) — plus succeeded() and step($name) |
| `JobStep` | number, name, status, conclusion, startedAt, completedAt — plus succeeded() |
| `DispatchedWorkflow` | provider, workflow, ref, runId, apiUrl, url (null when GitHub didn’t say), dispatchedAt |
| `WorkflowStatus` | Requested, Queued, Pending, Waiting, InProgress, Completed, Unknown |
| `WorkflowConclusion` | Success, Failure, Neutral, Cancelled, Skipped, TimedOut, ActionRequired, Stale, StartupFailure, Unknown |

A run or job payload without a usable id throws InvalidArgumentException naming the field — an id is never made up.

## Permissions

A scoped GitHub App token needs these permissions per call:

| Call | Permission |
| --- | --- |
| `dispatch(), cancel()` | `actions: write` |
| `runs(), run(), jobs()` | `actions: read` |
| `compare(), branch(), activity()` | `contents: read` |

```php
use RoundlyConsulting\Git\Dto\Credentials\GithubAppToken;
use RoundlyConsulting\Git\Dto\Input\InstallationTokenScope;

$credential = GithubAppToken::for($appId, $installationId, $privateKey)->forScope(
    new InstallationTokenScope(repositories: ['acme/infra'], permissions: ['actions' => 'write']),
);

Git::github($credential)->repo('acme/infra')->actions()->dispatch('deploy.yml', 'main');
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/git-for-laravel.md)
