# Deploy and roll back

## When to use this

You want new code live, or you want the previous version back. Scopes: `deployments:write` to start, `deployments:read` to follow.

## Deploy

```bash
curl -s -X POST $MYFORGE/sites/SITE_ID/deploy \
  -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: deploy-shop-20261009-1"
```

The answer is `202` and a deployment:

| Field | Meaning |
| --- | --- |
| `id` | Public id of the deployment. |
| `kind` | `deploy` or `rollback`. |
| `status` | `queued`, `running`, `succeeded` or `failed`. |
| `branch`, `commit`, `commit_message` | What is being deployed. |
| `started_by` | The person (or empty for a push). |
| `started_at`, `finished_at`, `duration_seconds` | Timing. |
| `code_findings` | Present when the code scan found something. |
| `log` | Last lines of the output (only in the single-deployment answer, or with `?log=1`). |

Deploys use releases: the new version is built next to the live one and the `current` link switches only when it is ready, so there is no downtime. Imported sites can deploy in place.

## Follow it

```bash
curl -s -H "Authorization: Bearer $TOKEN" $MYFORGE/sites/SITE_ID/deployments/DEPLOYMENT_ID
```

Poll every 10 seconds until `succeeded` or `failed`. `GET /sites/{site}/deployments` lists the latest 25 (newest first).

## Roll back

```bash
curl -s -X POST $MYFORGE/sites/SITE_ID/rollback \
  -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: rollback-shop-20261009-1"
```

This makes a deployment of kind `rollback` that switches to the previous release. Follow it like any deployment. Rollback does not undo database changes. Restore a backup for that: [Backups and restore](/developers/backups-and-restore).

## When a deploy is refused: 409

- The site is not `active`, is watch-only, or has no repository yet.
- A deployment is already queued or running, or the site is being moved.

## Automatic deploys

- **GitHub App:** sites connected to a GitHub repository deploy when someone pushes to their branch, if `auto_deploy` is on.
- **Webhook:** any site with `auto_deploy` on has a webhook URL. See [Webhooks](/developers/webhooks).
- Turn it on with `PATCH /sites/{site}` and `{"auto_deploy": true}`. The answer shows the webhook secret once.

## Health check

Set `health_check_path` (for example `/up`) with `PATCH /sites/{site}`. After a release is built pfcloud requests that path locally. If it fails, pfcloud switches back to the previous release and the deployment fails.

## The code scan

Before any build step runs, every deploy is scanned for malicious code: web shells, `rm -rf /`, reverse shells, miners, and install hooks that download code. If something serious is found:

- The deployment `status` is `failed` and `code_findings` lists each finding as `{severity, rule, location, detail}`, where `severity` is `critical` or `warning` and `location` is `file:line`.
- Nothing changes on the live site.
- The team's owners and admins are emailed.

Suspicious-but-not-proven items are shown as warnings and do not stop the deploy.

What to do: read the findings, look at the commit in Git, and find out who wrote it. If the code is yours and safe, a team member who can manage the site opens the failed deployment in the dashboard (site, Deployments) and approves that exact commit ("I reviewed it, approve this commit"), then deploys again. Approval is a human decision on purpose: an API token cannot approve a commit. The approval is recorded in the audit log. Details: the in-app help guide "Code scan before every deploy".

> An AI assistant that sees `code_findings` should stop and tell a person. It must not try to work around the scan.

## Environments and promote

A site can have environments such as staging, each a full site linked to the production site.

```bash
curl -s -H "Authorization: Bearer $TOKEN" $MYFORGE/sites/SITE_ID/environments
curl -s -X POST $MYFORGE/sites/SITE_ID/environments \
  -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: env-staging-1" -H "Content-Type: application/json" \
  -d '{"name":"staging","domain":"staging.example.com","server":"SERVER_ID","branch":"develop","create_database":true,"copy_env":true}'
```

`copy_env` starts the `.env` from production's, without the database, app key, URL and similar values. Environments count as sites on your plan.

To put an environment's last good commit on production:

```bash
curl -s -X POST $MYFORGE/sites/PRODUCTION_SITE_ID/environments/ENVIRONMENT_SITE_ID/promote \
  -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: promote-1"
```

This needs `deployments:write`. It answers `202`, or `409` when there is nothing to promote. Pull request previews are set with `PUT /sites/{site}/previews`. All endpoints: [Environments](/developers/api-environments).

## Deploy from CI

A CI step needs only `sites:read`, `deployments:read`, `deployments:write`, ideally limited to one site and to the CI runner's IP address. Fire the deploy, poll until `succeeded`, and fail the job on `failed`. Examples are in [OpenAPI and clients](/developers/openapi-and-clients). The command line does the same: `php artisan myforge:deploy {site} --wait`.
