# Fix a Failing CI Build in a Fastify API

Yes. A failed workflow starts an agent that reads the log, reruns the build, lint and node:test steps against MySQL in a sandbox and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/fix-failing-ci/fastify/), in a Project that has your repository.

## What the agent does

The run starts from a `workflow_run.failure` event, which names the workflow, the head branch and the commit. The agent clones that commit and reads the failed job's log with `gh run view <run id> --log-failed`. In an API like fastify/demo there is one workflow that matters, `.github/workflows/ci.yml`, with a single `build-and-test` job on a matrix of Node 22 and 24 and a `mysql:8.4` service container. Its steps are in a fixed order: `npm i --ignore-scripts`, `npm run build -- --noEmit`, `npm run lint`, a generated `COOKIE_SECRET`, `touch .env`, then `npm run db:migrate && npm run test`. The agent finds which step printed the first error, because a type error in `npm run build` and an assertion in `npm test` need different work. `lock-threads.yml` runs on a monthly cron and is never the cause.

It rebuilds the job's environment rather than guessing. MySQL 8.4 starts from `docker-compose.yml`, and the agent exports what the workflow's `Test` step sets: `MYSQL_HOST=localhost`, `MYSQL_PORT=3306`, `MYSQL_DATABASE=test_db`, `MYSQL_USER=test_user`, `MYSQL_PASSWORD=test_password`, `COOKIE_NAME=sessid`, `RATE_LIMIT_MAX=4` and `CAN_SEED_DATABASE=1`. Two of those are easy to miss and each breaks the suite on its own. `src/plugins/external/env.ts` lists `COOKIE_SECRET`, `COOKIE_NAME` and `COOKIE_SECURED` as required, so a missing secret fails at `app.ready()` in every test. And `scripts/seed-database.ts` throws unless `CAN_SEED_DATABASE` is `1`, which makes `npm test` fail before a single test runs, since the `test` script is `npm run db:seed && c8 npm run test:run`.

With the environment in place the agent reproduces the failure with the job's own command, then narrows it. `test:run` is `glob -c "tsx --test" "./test/**/*.ts"`, so one file runs as `npx tsx --test test/routes/api/tasks/tasks.test.ts`. The suite depends on the seeded `basic`, `moderator` and `admin` users and on `RATE_LIMIT_MAX=4`: `test/app/rate-limit.test.ts` sends four requests and expects the fifth to be `429`, and the first case in `test/routes/api/users/users.test.ts` expects `PUT /api/users/update-password` to refuse a fourth attempt inside a minute, from the per-route `rateLimit` of `max: 3` in `src/routes/api/users/index.ts`. A failure in those files after a change to `src/plugins/external/rate-limit.ts` is a changed limit, not a flaky test. A failure in `tasks.test.ts` after a change to `src/schemas/tasks.ts` is usually a `response` schema that no longer lists a returned column, because Ajv in `src/server.ts` runs with `removeAdditional: 'all'`.

The fix goes where the cause is: `src/`, a migration under `migrations/`, or the test when the test is wrong, and the pull request says which. For a lint failure the agent runs `npm run lint:fix` and keeps only the lines the failing commit touched; `eslint.config.js` extends neostandard with a `comma-dangle: never` rule, so a trailing comma is the usual culprit. For a type error it runs `npm run build -- --noEmit` and reads the file and line from `tsc`. It reruns build, lint, migrate and the full `npm test` before opening the pull request, and it never raises `RATE_LIMIT_MAX`, skips a test or adds `--ignore-scripts` to anything beyond the install step the workflow already uses.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "The GitHub Actions workflow on this branch failed; the workflow_run event follows this prompt. Read the failed job's log with gh run view <run id> --log-failed and quote the first failing step and error. Rebuild the job's environment from .github/workflows/ci.yml: start MySQL 8.4 as docker-compose.yml describes, export MYSQL_HOST, MYSQL_PORT, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, a generated COOKIE_SECRET, COOKIE_NAME, RATE_LIMIT_MAX=4 and CAN_SEED_DATABASE=1, and create an empty .env. Run npm i --ignore-scripts, npm run build -- --noEmit, npm run lint, npm run db:migrate and npm test, running the one failing test file alone with npx tsx --test first. Fix the cause in src, migrations or the test if the test is wrong, and say which. Rerun all five steps until they pass. Do not raise RATE_LIMIT_MAX, skip a test, or weaken a TypeBox schema to make a test pass. Open a pull request against the failing branch that quotes the error it fixed and the passing output.\n\nEvent: <workflow_run payload>",
  "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 against the branch that failed. Its description names the workflow and the first failing step, quotes the error from the log, and shows the same command failing and then passing in the sandbox, with the Node version from `node --version`. The diff is the fix. A change to `package.json`, `eslint.config.js` or `ci.yml` is listed apart from the code because it changes every future build.

The repository's own `CI` workflow runs on the pull request, so the reviewer sees the Node 22 and Node 24 rows go green on GitHub rather than taking the sandbox's word for it. They check that no test lost an assertion, that `RATE_LIMIT_MAX` and the per-route limit in `src/routes/api/users/index.ts` are what they were, and that a TypeBox `response` schema was widened only when the column really belongs in the reply.

When the cause is outside the code, such as a registry outage during `npm i` or a MySQL image pull that fails, the run says so with the log lines it read and opens no pull request, instead of changing something unrelated to turn the check green.

## Run it on every failed CI run on a branch

In the console, add an automation to the Project for this repository with this prompt and a `github` trigger on `workflow_run.failure`. The trigger appends the event after the prompt, so the run knows the workflow, branch and commit. Only runs on branches by an owner, member or collaborator fire it, and a failure on a fork never does. The automation's `max_runs_per_hour` keeps a bad dependency bump that reddens several branches from starting more runs than you want.

If your own server routes CI failures instead, it can send this body to `POST /o/{org}/sessions` with the failing branch as `https://github.com/<owner>/<repo>/tree/<head_branch>` in `resources`, the GitHub delivery id as the `Idempotency-Key`, and the event text after the prompt. The key's `webhook_url` then receives `run.completed` with a `session_url` that reads the pull request.

## Why run it on Rush?

A red Fastify build is rarely reproduced on the laptop that caused it, because the laptop has a `.env`, a local MySQL with yesterday's rows and a Node version that is not the one in the matrix. The sandbox starts from the workflow file: the same `mysql:8.4` image, the same variables, migrations from zero, and the same `tsx --test` runner. Every command and its output stay on the session, so the pull request can quote the failure and the pass rather than describe them.

The suite is sensitive in ways that need the environment to be exact, such as `RATE_LIMIT_MAX=4` and `CAN_SEED_DATABASE=1`, so proving a fix means running it there. When several branches fail at once, each gets its own run and its own container, and each ends with a pull request whose checks GitHub confirms, or a note saying what could not be reproduced.

## Frequently asked questions

### Which Node version does the sandbox use?

The workflow runs Node 22 and 24, and the sandbox runs one of them and names it. The pull request's own CI run covers the other row. If the log shows the failure on only one version, the agent installs that one to reproduce it.

### The failure is in the seed step, not a test. Can it fix that?

Yes. `npm test` runs `db:seed` first, and `scripts/seed-database.ts` refuses to run unless `CAN_SEED_DATABASE=1` and truncates every table before inserting users and roles. A seed failure is usually a migration the seed does not expect, and the fix is in `migrations/` or the seed script.

### Will it change the rate limit to make the 429 tests pass?

No. `test/app/rate-limit.test.ts` and the first case in `users.test.ts` encode `RATE_LIMIT_MAX=4` and the `max: 3` on the password route. The prompt forbids raising either, so a test that fails there means the limiter changed and the pull request fixes the limiter.

### What if the lint failure is in a file the commit did not touch?

The agent fixes it, since `npm run lint` runs on the whole tree and the branch cannot go green otherwise, but it lists that change separately in the pull request and keeps it to what `lint:fix` produced.
