# Push and publish a version

> Send the files of a version, fix what the validation found, run an action to try it, put the version in production, roll back, and find out what failed.

Once the files are written, a version goes live in a few calls: push it, fix what the validation
found, try one of its actions, then put it in production.

The examples use `$TOKEN`, `$API_KEY` and `$APP_ID` as [Build an
application](/docs/build-an-application/#the-credentials-in-the-examples) defines them.

## Push a version

### With the files on a machine

Reserve a deposit, then PUT the archive to the URL it returns. The URL carries its own authorisation
and works once, so the script that uploads holds no key of yours.

```sh
VERSION=$(curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/versions" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"mode":"archive"}')

URL=$(printf '%s' "$VERSION" | jq -r '.upload.url')
VERSION_ID=$(printf '%s' "$VERSION" | jq -r '.version_id')

cd project && zip -qr - . | curl -s -X PUT "https://api.eu.apigami.com$URL" \
  -H 'Content-Type: application/zip' --data-binary @-

curl -s "https://api.eu.apigami.com/v1/apps/$APP_ID/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN"
```

Reserving receives nothing: the version exists once the PUT lands.

### Without a terminal

Send the changes instead of the files. `POST /v1/apps/{app}/versions` with `base_version_id` and a
list of changes builds a new version from an existing one.

```json title="Request body"
{
  "base_version_id": "VERSION_ID",
  "changes": [
    { "op": "write", "path": "actions/basket.ts", "content": "export const readBasket = …" },
    { "op": "replace", "path": "main.ts", "old_text": "'POST /search'", "new_text": "'POST /search'" },
    { "op": "delete", "path": "actions/old.ts" }
  ]
}
```

A `replace` whose `old_text` appears zero times or more than once is refused, and nothing is
written: an edit that lands in the wrong place is worse than an edit that does not land.

Only bytes your environment can actually read can be sent. An image you can see in a conversation is
not a file you can upload — ask for its path, or have the file written as text.

### Add one file to a version

A logo, or a corrected manifest, without resending the rest: reserve a deposit with `mode: "file"`,
a `base_version_id` and the `path` the bytes land on, then PUT the bytes.

```sh
curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/versions" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"mode":"file","base_version_id":"'"$VERSION_ID"'","path":"public/logo.svg"}'
```

## Fix what the validation found

Receiving a version and being a valid project are two different results. A version whose manifest is
missing or wrong is kept, and its diagnostics name the file, the field and the fix:

```json
{
  "version_id": "…",
  "state": "ready",
  "validation": {
    "status": "invalid",
    "errors": 1,
    "diagnostics": [
      {
        "code": "missing_operation_id",
        "file": "manifest.json",
        "pointer": "/paths/~1basket/post",
        "message": "Give this operation an `operationId`: it is the name of the action."
      }
    ]
  }
}
```

Fix the file it names with one `replace`, on the same `base_version_id`. Nothing unchanged is sent
again, and you never have to read the whole project back to correct one line.

## Run it, deploy it, publish it

```sh
# Run an action of a precise version — this is how you try one. It takes an API key of the application.
curl -s -X POST "https://api.eu.apigami.com/v1/apps/$APP_ID/actions/searchProducts" \
  -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
  -H "X-Apigami-Version: $VERSION_ID" \
  -d '{"query":"coffee"}'

# Deploy it, and check its health.
curl -s -X PATCH "https://api.eu.apigami.com/v1/apps/$APP_ID/versions/$VERSION_ID" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"deployed":true}'

# Put it in production. The same call on an earlier version is the rollback.
curl -s -X PATCH "https://api.eu.apigami.com/v1/apps/$APP_ID/versions/$VERSION_ID" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"production":true}'
```

**Work in bounded chunks.** Apigami waits up to 900 seconds for an action, fifteen minutes, and less
when another run calls it: a child never outlives its parent. Within that time, each request has 300 seconds of
computing time, and an instance 128 MB of memory shared by the requests it serves at once
([Limits](/docs/app-server/#limits)); past either, the run fails with a sentence that names the limit. Its caller waits thirty seconds, then gets `202 { run_id, state: "running" }`
while the run goes on; `GET /v1/runs/{run_id}` reads its outcome with a member's credential, never with a key
or a page's token. So have each run do a short chunk, save its result and its progress in the database, and
let a trigger or the next call carry on from there.

There is no try route: trying is running, with `X-Apigami-Version` naming the version. A member of
the owning group may name one; everyone else always reaches the version in production.

A version that has no deployment yet runs in a sandbox, a trial, which is what lets you try code before
you deploy it. The answer says which of the two served it, and only a run served by the version's own
deployment that answered below `500` counts towards publishing.

Publication is refused in three cases, and each one says what to do: a version none of whose actions
has answered below `500` on its own deployment, a version whose last `GET /healthz` did not answer
200, and a version one of whose pages answered an error (`page_failed`). After `GET /healthz`, a
deployment asks each page with a relative `page_url` once, with no token and no cookie, as anyone
could; a page answering `400` or more, or loading `apigami.js` from `/apigami.js` on your app's own
address, blocks publication, and publishing asks it again first. A page
answering `200` with a body is not refused, but the answer reminds you that anyone can read what it
rendered on the server. A deployment answers with a `publish` block carrying `publishable`, what is blocking and the one
sentence to act on, so you do not have to ask a second time.

Deploying and publishing are one route, `PATCH /v1/apps/{app_id}/versions/{version_id}`, whose body
takes `deployed: true`, `production: true` or both, and nothing else. Sent together, the deployment
comes first, but a version deployed for the first time has never been run, so the publication answers
`409`: run an action between the two. With the MCP tools, `app_deploy` and `app_publish` take
`app_id` and `version_id`, and `app_publish` on an earlier version is the rollback. The answer is the
version: its `state`, `production`, `health`, `tried_at`, `review` and a `publish` block with
`publishable`, its `blockers` and the `next` step.

An application that is `unlisted` or `public` goes to an Apigami administrator for review before another
group may call it. Publishing it is a request for review: production does not change, for anyone, your
group included, and the answer carries `"review": { "state": "pending_review" }` with a `note`. An
administrator then puts the version in production, or answers `rejected` with a `reason`. Every
version you publish is reviewed, a rollback included; a `private` application is published at once.
`app_status` (`GET /v1/apps/{app}`) follows the review, and you can write to
[contact@apigami.com](mailto:contact@apigami.com) about it.

### Visibility: private, unlisted or public

Who may connect is the application's `visibility`: `private`, your group only, the default;
`unlisted`, anyone with its link; `public`, also listed in the catalogue. Opening it to others is the
person's decision: an assistant asks before raising it, and never lowers it on its own. It is
`app_settings` with `app_id` and `visibility`, or:

```sh
curl -s -X PATCH "https://api.eu.apigami.com/v1/apps/$APP_ID" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"visibility":"unlisted"}'
```

It answers the application. `POST /v1/apps` takes the same field at creation,
`{"name":"Watch","visibility":"private"}`; any field these two routes do not know answers `400`. Once
the application is `unlisted` or `public`, each publication is a request for review, as above. A version
already in production when you raise it, published while the application was private, was never reviewed: until a
reviewed version replaces it, it keeps serving your group alone, outside the catalogue, and anyone else is refused
with `403 app not reviewed`. Publish a new version to ask for that review.

### Try an action that reaches a site through the browser

An action that uses a site session runs in one of your user's browsers. With your application's key,
name the user with `X-Apigami-User-Ref`, their `user_ref`, and to pick one of their browsers add
`X-Apigami-Install-Ref`; without it, Apigami takes the browser `POST /call` would take for the first
site the version reaches through a browser. Both are headers, because the body and the query string
are your action's own. Under a run token neither is accepted: the run already names its browser. The same
holds for the body of `POST /call`: with your application's key, `user_ref` is required and `install_ref`
optional; when your action calls it with the token handed in `X-Apigami-Run`, leave both out, since naming
one answers `400`.

The first time you try such an action on a site, the person may see Apigami's approval window for that
site, even just after connecting to your application: the connection approves your sites in the same
click only when it attached their browser in Apigami's window ([Site approval](/docs/consent/)). Tell
them to expect it and to click **Authorize**; until they answer, the action's `POST /call` gets `403`
with `consentPending: true`, and running it again afterwards goes through.


## When something fails

Three places to look, and they answer different questions:

- `GET /v1/apps/{app}/logs?version=…` — what a deployed version printed. A version whose deployment
  was refused printed nothing there: the refusal says why.
- `GET /v1/runs/{run}` — what the invocation returned: its state, the status your code answered,
  which environment served it, and the lines a trial kept when a sandbox ran it.
- `GET /v1/apps/{app}/runs?version=…` — what ran and what failed, including a trigger that fired
  with nobody watching.

What your code wrote to its own database is read with `POST /v1/apps/{app}/database/queries`:
Apigami is not on that path.

### A deployment that failed: its build, or its server at startup

A deployment that could not be brought up answers `422`, with the step that failed and what it printed:

- **the build**: an import that does not resolve, a package refused, with the end of the build's log;
- **the migrations**: the file and the statement that failed ([Migrations](/docs/app-database/#migrations-migrations));
- **the code as it loads**: input or output at the top level of a file, an exception thrown there, or
  `[apigami] Export default { fetch(request, env, ctx) } from your entrypoint.`

Nothing ran, so `GET /v1/apps/{app}/logs?version=…` (`app_logs`) has nothing from it. Run one of its actions
with `X-Apigami-Version`: a version with no deployment runs in a sandbox, whose answer and run
(`GET /v1/apps/{app}/runs`, `app_runs`) carry the same failure and what the code printed. A version written
for manifest v1 is refused with `409` before any of this ([Retired fields](/docs/app-manifest/#retired-fields)).