All jobs

Turn Labeled GitHub Issues Into Pull Requests for a Laravel App

Yes. Label a GitHub issue on a Laravel app and an agent reproduces it with a PHPUnit test, fixes the code, runs the suite and opens a pull request that closes it.

Run this on RushIn a Project that has your repository.

What the agent does

The agent starts from the labelled issue, not from a generic Laravel layout. In a personal CRM shaped like monicahq/monica it reads composer.json first: php ^8.3 with the fileinfo and intl extensions, laravel/framework ^12.0, inertiajs/inertia-laravel ^2.0 for the pages, laravel/jetstream, fortify and sanctum for auth, laravel/scout for search and knuckleswtf/scribe for the API docs; on the dev side phpunit, brianium/paratest, larastan, laravel/pint and psalm. .tool-versions pins php 8.3.16, nodejs 22.13.1 and yarn 4.6.0. phpunit.xml puts tests/Feature and tests/Unit in one suite, sets DB_CONNECTION to testing, the queue to sync, the scout driver to collection and disables Pulse and Telescope, and config/database.php resolves the testing connection to SQLite at database/database-test.sqlite unless DB_TEST_DRIVER says otherwise.

The issue text decides the directory. The code is organised by domain under app/Domains, with Contact, Settings and Vault at the top and a folder per feature inside Contact, such as ManageContact, ManageContactAddresses, ManageNotes, ManageReminders and ManageRelationships. Each feature splits into Services, Web/Controllers, Web/ViewHelpers and, where it syncs with CardDAV, Dav. A report about creating or updating a contact leads to app/Domains/Contact/ManageContact/Services, where CreateContact extends BaseService, declares its rules() for account_id, vault_id, author_id, the name fields, gender_id, pronoun_id and template_id, and its permissions() as author_must_belong_to_account, vault_must_belong_to_account and author_must_be_vault_editor, then runs validate, createContact, updateLastEditedDate and createFeedItem. A report about the page itself leads to Web/Controllers, where ContactController builds the index query from the user's name order and sort order, paginates by 25 and renders Vault/Contact/Index through Inertia with data from ContactIndexViewHelper. The agent fixes the service when the rule is wrong and the controller or view helper when the page is.

The test comes before the fix. tests/Unit/Domains/Contact/ManageContact/Services/CreateContactTest.php is the pattern: it uses DatabaseTransactions, builds a user with createUser, a vault with createVault, grants a permission with setPermissionInVault, and executes the service through a helper that asserts the created rows and the feed item. A permission bug gets a case that expects NotEnoughPermissionException; a validation bug gets one that expects ValidationException. The agent adds a method to the existing test class for the service it changes rather than a new file, and marks it with the @test annotation the class already uses.

It runs what CI runs. .github/workflows/tests.yml hands the job to the reusable monicahq/workflows laravel.yml, which runs the suite on PHP 8.3 and 8.4 against sqlite, mysql and pgsql with sqlite as the default, and separate workflows run static analysis and lint. In the sandbox the agent runs composer install, then php artisan test on the test file first and then the whole suite on SQLite, then vendor/bin/phpstan through larastan and vendor/bin/pint --test on the files it changed. It leaves the Vue pages under resources/js alone unless the issue is about them, and opens a pull request whose body starts with Closes and the issue number and explains the cause.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Fix GitHub issue #ISSUE in this Laravel repository. Read the issue and its comments first. Start from composer.json, .tool-versions, phpunit.xml and the testing connection in config/database.php, then find the domain folder the report names under app/Domains/Contact, reading its Services, Web/Controllers and Web/ViewHelpers. Add a test method to the existing test class for the service or controller you change under tests/Unit or tests/Feature, using the createUser, createVault and setPermissionInVault helpers the neighbouring tests use, confirm it fails, then fix the code until php artisan test passes on SQLite, vendor/bin/phpstan passes and vendor/bin/pint --test passes on the files you changed. Do not change the PHP version, composer.lock or any workflow file. Open a pull request whose body starts with Closes #ISSUE, explains the cause, lists the files and quotes the test summary.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is a pull request on a new branch. The diff is the service, controller or view helper the issue pointed at under app/Domains, plus one method added to the matching test class under tests/Unit or tests/Feature. The body starts with Closes and the issue number, names the cause in a sentence, lists the files, and quotes the php artisan test summary, the phpstan result and the pint result from the sandbox. composer.lock, .tool-versions and the workflows are unchanged unless the issue itself asked for them.

The session read names the repository and the pull request URL. The transcript at GET /o/{org}/sessions/{id}/messages keeps each tool call: the issue read, the files opened, composer install, and each test invocation with its output, including the first run where the new method fails. A reviewer does not have to re-run the suite to see what the agent ran.

Before merging, a reviewer checks that the new method fails on the old code and passes on the branch, that a permission fix went into the service's permissions() list or BaseService rather than a check in the controller, that no rule was loosened in rules() to make the test pass, and that the GitHub keyword in the body points at the right issue, since it closes the issue on merge. CI then runs the same suite on MySQL and PostgreSQL as well.

Run it on every labelled GitHub issue

Add an automation to the Project with this prompt and a github trigger on issues.labeled, with the label you reserve for the agent in its labels. Only a label applied by an owner, member or collaborator fires it. Each labelled issue starts one run on its own branch, and the trigger adds the issue event after the prompt, so the agent reads the number from it. From your own server, the same run starts with this request when your GitHub webhook handler sees the label, with an Idempotency-Key derived from the delivery id so a retried delivery returns the same session.

Give the API key a webhook URL and Rush POSTs run.completed, run.failed or run.cancelled when the turn ends, signed with the whsec_ secret, with data.session_url to read for the pull request. A key runs at most max_concurrent sessions at once, 4 by default, and answers 429 past that.

The use case this job follows, with its automation and trigger: Turn a Linear label into a PR.

Why run it on Rush?

Reproducing a service bug on a laptop means PHP 8.3 with the intl and fileinfo extensions, a Composer install with scribe and sabre in it, and remembering which helper grants vault permissions in the test base class. The agent does that in its own sandbox with its own clone, and the sandbox parks with its disk when the turn ends, so a follow-up from the review picks up the same environment.

The proof is the pull request plus the transcript, not a claim that the tests passed. The exact test output, the failing first run and the test class the new method landed in are kept on the session. Several labelled issues run at once, each in its own sandbox with its own pull request, up to the key's concurrency limit.

Frequently asked questions

Does it test against MySQL and PostgreSQL too?

In the sandbox it runs the suite on SQLite, the default the testing connection and CI both use. The reusable workflow then runs MySQL and PostgreSQL on the pull request. A query that only fails on one of them shows up there, and a follow-up on the session can fix it.

Will it change a Vue page under resources/js?

Only when the issue is about the page. The Inertia controllers pass data through view helpers, so most display bugs are fixed in the helper and its unit test, not in the component. Say so in the issue when the component itself must change.

What about the CardDAV import and export code?

Each feature's Dav folder has importers and exporters with their own tests, such as ImportContact and ExportAddress under ManageContact. A sync bug is fixed there, with a case in the matching Dav test class.

How does the issue text reach the agent?

A handler you run writes the issue number into input in place of #ISSUE; an automation's github trigger instead adds the issue event after the prompt and replaces nothing, and resources clones the repository through the Rush GitHub App installation. The agent then reads the issue and its comments from the repository's issue tracker with the credentials the installation provides.