# Turn GitHub Issues into Pull Requests for a Fastify API

Yes. Label a GitHub issue on a Fastify API and an agent reproduces it with a node:test case against MySQL, fixes the route and opens a pull request that closes it.

[Run this on Rush](https://cloud.getrush.ai/run/github-issue-to-pr/fastify/), in a Project that has your repository.

## What the agent does

The agent reads the issue, then reads how the app is wired before it edits a route. In an API like fastify/demo, `src/app.ts` loads `src/plugins/external/` first (env, knex, session, rate-limit, swagger, under-pressure and the rest), then `src/plugins/app/` (the repositories, `password-manager.ts`, `authorization.ts`, `file-manager.ts`), then `src/routes/` with `autoHooks: true`. Routes are files, so `src/routes/api/tasks/index.ts` is `/api/tasks` and `src/routes/home.ts` is `/`. `src/routes/api/autohooks.ts` demands `request.session.user` on everything under `/api` except `/api/auth/login`. An issue that says a request returns `401` unexpectedly is answered by that one file, not by the route it names.

Each route declares a TypeBox schema, and the schema is where many reported bugs live. `src/schemas/tasks.ts` defines `TaskSchema` with `assigned_user_id` optional, `CreateTaskSchema` with `name` and `assigned_user_id`, and `QueryTaskPaginationSchema` with `page` at least 1, `limit` between 1 and 100 with a default of 10, and `order` defaulting to `desc`. `src/server.ts` runs Ajv with `coerceTypes: 'array'` and `removeAdditional: 'all'`, so a field the issue says is ignored is usually one the schema does not list. `src/schemas/users.ts` requires a password of at least 8 characters with an upper case letter, a lower case letter, a digit and a symbol, and `src/schemas/common.ts` caps strings at 255 characters, which is also the `VARCHAR(255)` width in `migrations/001.do.users.sql` and `002.do.tasks.sql`.

The reproduction is a `node:test` case in the file that already covers the route. `test/helper.ts` exports `build()`, which registers the app through `fastify-plugin` so decorators are visible, plus `app.login(email)` and `app.injectWithLogin(email, opts)`, which post to `/api/auth/login` and attach the session cookie named by `COOKIE_NAME`. The seeded users from `scripts/seed-database.ts` are `basic@example.com`, `moderator@example.com` and `admin@example.com`, all with `Password123$`, and the roles nest so that admin holds all three. A test for a moderator-only path such as `POST /api/tasks/:id/assign` logs in as `moderator`; a test for `DELETE /api/tasks/:id` logs in as `admin`, because `src/plugins/app/authorization.ts` sends `403` otherwise. The new test fails on the issue's input first, and the agent quotes that failure in the pull request.

The fix is small and stays in the layer the issue names: the route handler, the repository in `src/plugins/app/tasks/tasks-repository.ts` (where `paginate` orders by `created_at` then `id` to break ties, and `update` returns `null` when no row changed), or the schema. A column change gets a numbered pair of `.do` and `.undo` files in `migrations/`, because `scripts/migrate.ts` runs Postgrator over that directory. Then the agent runs the five steps `.github/workflows/ci.yml` runs, `npm i --ignore-scripts`, `npm run build -- --noEmit`, `npm run lint`, `npm run db:migrate` and `npm test` with MySQL 8.4 from `docker-compose.yml` and `RATE_LIMIT_MAX=4` and `CAN_SEED_DATABASE=1` exported, and opens a pull request whose body says `Closes #<issue>`.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Fix the GitHub issue that follows this prompt in this Fastify 5 API. Read src/app.ts for the plugin load order and src/routes/api/autohooks.ts for the session check before touching a route. Reproduce the reported behavior with a node:test case in the existing test file for that route, using build(), app.inject and app.injectWithLogin from test/helper.ts and the seeded basic, moderator and admin users. Run it alone with npx tsx --test <file> and confirm it fails. Fix the cause in the route, the repository or the TypeBox schema; add a numbered .do and .undo migration if a column changes. Start MySQL 8.4 from docker-compose.yml, export the variables .github/workflows/ci.yml sets including RATE_LIMIT_MAX=4 and CAN_SEED_DATABASE=1, create an empty .env, and run npm i --ignore-scripts, npm run build -- --noEmit, npm run lint, npm run db:migrate and npm test until all pass. Open a pull request whose description says Closes #<issue>, explains the cause and quotes the failing then passing test.\n\nIssue #57: Listing tasks with ?limit=100&page=2 returns tasks from page 1 again when two tasks were created in the same second.",
  "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 with `Closes #<issue>` in its body. The diff holds the new test case in the existing file under `test/routes/api/`, the change in `src/`, and when a column moved, a new numbered pair in `migrations/`. The description names the cause in the words of the code, such as a `response` schema that dropped a field or a `where` clause in the repository, and quotes the test failing before the fix and the `npm test` summary after it.

The reviewer reads the test first and checks it would fail on the main branch. Then they check the fix stayed in scope: no change to `autohooks.ts` to make a request pass, no widened password pattern in `src/schemas/users.ts`, no `.do` file without its `.undo`. The pull request's own `CI` workflow runs the suite on Node 22 and 24 against a fresh MySQL, so the sandbox's result is confirmed on GitHub before merge.

The session keeps the whole record: the `tsx --test` run that failed, the edits, the migration output and the final `c8` table. `GET /o/{org}/sessions/{id}` returns the pull request on the session once the turn has finished, and a follow-up can ask the same sandbox to add a second case to the test.

## Run it on every GitHub issue labelled agent

In the console, add an automation to the Project with this prompt and a `github` trigger on `issues.labeled`, filtered to the `agent` label in `config.labels`. The trigger appends the issue after the prompt, so you leave the example issue out of the automation's prompt. Only labels added by an owner, member or collaborator fire it, by GitHub's `author_association`, so a stranger's issue cannot start a run. Each labelled issue gets its own sandbox, branch and pull request.

The same request can come from your own issue tracker or triage bot through `POST /o/{org}/sessions`, with the issue's text after the prompt and the delivery id as the `Idempotency-Key`. Set a `webhook_url` on the key and Rush POSTs `run.completed` when the turn finishes, with the `session_url` that reads the pull request.

## Why run it on Rush?

A reproduction in this codebase needs MySQL 8.4, five migrations, a seed with three users and roles, a session cookie and the exact environment the suite expects, including `CAN_SEED_DATABASE=1` or `npm test` stops before the first case. That is an afternoon on a laptop that has a different database running. A Rush run builds it from the repository's own files in its own sandbox and keeps every command, so the pull request shows the failing test rather than asserting there was one.

The fix is proportionate because the agent reads the files that define behaviour, `autohooks.ts`, `authorization.ts`, the schemas and the repository, before changing a line. Several labelled issues run at once, each on its own branch, and each ends in a pull request whose own CI confirms what the sandbox saw, or in a note that explains why the issue could not be reproduced.

## Frequently asked questions

### Does the test need a real MySQL?

Yes. The suite runs `app.inject` against seeded tables through Knex, and `test/helper.ts` builds the whole app. The agent starts `mysql:8.4` from `docker-compose.yml` in the sandbox as CI's service container does, applies the migrations with `npm run db:migrate` and seeds with `npm run db:seed` through `npm test`.

### The issue is about a 401. Will it edit the auth hook?

It reads `src/routes/api/autohooks.ts` first, which returns `401` for every `/api` route without a session except the login route. If the issue's route is meant to be public, the fix is to place it outside `src/routes/api/`, as `src/routes/home.ts` is, not to loosen the hook.

### What if the fix needs a new column?

The agent adds a numbered `.do` and `.undo` pair in `migrations/`, as `003.do.user_tasks.sql` and `003.undo.user_tasks.sql` are paired, updates the TypeBox schema in `src/schemas/` so the response lists the column, and runs `npm run db:migrate` before the suite.

### Which user does the test log in as?

The one the route requires. `DELETE /api/tasks/:id` has `request.isAdmin` as a `preHandler` and `POST /api/tasks/:id/assign` has `request.isModerator`, both from `src/plugins/app/authorization.ts`. The seed gives `admin` every role and `moderator` two, so the test uses the least privileged user that passes.

### Will it touch the OpenAPI document?

Only if the fix adds or changes a route's schema. `src/plugins/external/swagger.ts` builds the document from route schemas with `hideUntagged: true`, so a new route gets `tags` so it appears at `/api/docs`, and the pull request says so.
