# Despliega desde CI con un token de API

Todas las guías de esta sección hacen las mismas cuatro cosas, en cualquier sistema de CI:

1. construir una imagen y subirla a un registro, con **una etiqueta inmutable por commit**;
2. decirle a Zaroz que ejecute esa imagen (`PUT /service-instances/{id}/source`);
3. iniciar un despliegue y esperar a que termine (`POST /service-instances/{id}/deployments`);
4. hacer fallar el trabajo si el despliegue falló y volver a poner la imagen anterior.

Los pasos 2 a 4 son un pequeño script, [`zaroz-deploy.sh`](/es/guides/ci/shell). Las guías de GitHub, GitLab y Forgejo solo se diferencian en cómo construyen, dónde viven los secretos y cómo evitan que dos despliegues corran a la vez.

| Sistema de CI | Guía |
|---|---|
| GitHub Actions | [GitHub Actions](/es/guides/ci/github-actions) |
| GitLab CI | [GitLab CI](/es/guides/ci/gitlab-ci) |
| Forgejo Actions y Gitea Actions | [Forgejo y Gitea Actions](/es/guides/ci/forgejo-actions) |
| cualquier otro (Jenkins, Drone, un cron en tu servidor) | [Shell genérico](/es/guides/ci/shell) |

## Crea un token que solo pueda desplegar

CI nunca debería tener un token que pueda leer tus facturas o borrar tu cuenta. Dale un token limitado a **una instancia de servicio**.

:::note
**`services.deploy` es el permiso de despliegue.** Permite exactamente dos escrituras sobre un servicio: `PUT /service-instances/{id}/source` y `POST /service-instances/{id}/deployments`. **No** lee nada, no cambia variables de entorno, copias de seguridad, archivos ni el cortafuegos, no inicia, detiene ni reinicia el servicio (`services.power`), no abre terminal y no toca la facturación ni los miembros. Añade `services.read` para que el trabajo pueda leer el source y esperar a que termine el despliegue. Qué token usar depende de lo estrecho que lo necesites:

- Un **token personal** (`POST /me/api-tokens`, abajo) se puede limitar a **una instancia de servicio**. Actúa como la persona que lo creó y para crearlo hace falta una sesión completa: un token de API no puede crear otro.
- Un **token de organización** (`POST /organizations/{id}/api-tokens`) pertenece a la organización y puede sobrevivir a quien lo creó (`"lifetime": "organization_owned"`), pero cada permiso se limita a **toda la organización**, nunca a un solo servicio. `GET /organizations/{id}/api-tokens/grantable-capabilities` lista lo que puedes poner en él. Úsalo para un pipeline de equipo que despliega varios servicios.
:::

**curl**

```bash
# 1. what your own account may grant
# (the header goes through curl's config on stdin so it never shows in `ps`)
zcurl() { printf 'header = "Authorization: Bearer %s"\n' "$ZAROZ_TOKEN" | curl -sS -K - "$@"; }
zcurl "$ZAROZ_API/me/api-token-capabilities"

# 2. a token that can only manage ONE service instance
zcurl -X POST "$ZAROZ_API/me/api-tokens" -H "Content-Type: application/json" -d '{
  "name": "ci-deploy-my-api",
  "expires_at": "2027-01-01T00:00:00Z",
  "authorization": {"grants": [
    {"capability": "services.deploy",
     "scope": {"kind": "resource", "resource_type": "service_instance", "resource_id": "<service instance uuid>"}},
    {"capability": "services.read",
     "scope": {"kind": "resource", "resource_type": "service_instance", "resource_id": "<service instance uuid>"}}
  ]}
}'
# {"token": {...}, "secret": "zaroz_..."}   the secret is shown this once
```

**TypeScript**

```ts
const res = await fetch(`${API}/me/api-tokens`, {
  method: "POST",
  headers: { Authorization: `Bearer ${ADMIN_JWT}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "ci-deploy-my-api",
    expires_at: "2027-01-01T00:00:00Z",
    authorization: {
      grants: ["services.deploy", "services.read"].map((capability) => ({
        capability,
        scope: { kind: "resource", resource_type: "service_instance", resource_id: SERVICE },
      })),
    },
  }),
});
console.log((await res.json()).secret);
```

**Python**

```python
r = requests.post(
    f"{API}/me/api-tokens",
    headers={"Authorization": "Bearer " + ADMIN_JWT},
    json={
        "name": "ci-deploy-my-api",
        "expires_at": "2027-01-01T00:00:00Z",
        "authorization": {"grants": [
            {"capability": c,
             "scope": {"kind": "resource", "resource_type": "service_instance", "resource_id": SERVICE}}
            for c in ("services.deploy", "services.read")
        ]},
    },
    timeout=20,
)
print(r.json()["secret"])
```


- `resource_id` es el id de la **instancia de servicio**, el mismo UUID que aparece en la URL del Workload en la consola.
- Dale al token un `expires_at` y pon un recordatorio para rotarlo. Un token personal se revoca con `DELETE /me/api-tokens/{token_id}`.
- El secreto (`zaroz_...`) se muestra **una sola vez**. Guárdalo directamente en el almacén de secretos de tu CI. Nunca lo pongas en el repositorio, en archivos `.env` que se suban ni en la imagen.
- Mantén el secreto fuera de los argumentos de la línea de comandos, que cualquier usuario de un runner compartido puede leer con `ps`. Pásalo como variable de entorno desde el almacén de secretos de CI, como hacen los flujos; el script se lo entrega a `curl` por la entrada estándar.
- Crea un token por servicio y por entorno (staging, producción), de modo que revocar uno no detenga a los demás.

Un token personal actúa como la persona que lo creó: si esa persona deja la organización o pierde el acceso al servicio, el token deja de funcionar. Para un equipo, prefiere un token de organización con `lifetime: "organization_owned"` (sobrevive a quien lo creó, pero cubre todos los servicios de la organización):

```bash
zcurl -X POST "$ZAROZ_API/organizations/<uuid de la organización>/api-tokens" -H "Content-Type: application/json" -d '{
  "name": "ci-deploy",
  "lifetime": "organization_owned",
  "expires_at": "2027-01-01T00:00:00Z",
  "capabilities": ["services.deploy", "services.read"]
}'
# {"token": {...}, "secret": "zaroz_pat_..."}   se muestra una sola vez
```

Para crearlo hace falta ser Owner o Admin. Si omites `lifetime`, el token se revoca cuando termina tu pertenencia.

## Qué hace el token en cada paso

| Paso | Petición | Permiso |
|---|---|---|
| Leer qué se ejecuta ahora | `GET /service-instances/{id}/source` | `services.read` |
| Elegir la imagen | `PUT /service-instances/{id}/source` | `services.deploy` |
| Desplegar | `POST /service-instances/{id}/deployments` | `services.deploy` |
| Esperar y comprobar | `GET /service-instances/{id}/deployments` | `services.read` |

`PUT /source` **no despliega**: solo guarda lo que bajará el siguiente despliegue. `POST /deployments` no lleva cuerpo y despliega lo guardado, así que no hay forma de desplegar algo que no se guardó antes.

## Despliegue, éxito y reversión

Un despliegue tiene un `status` (cómo terminó la operación) y un `outcome` (`succeeded` o `failed`, con un `failure_reason` como `cluster_rejected` o `subscription_not_active`). `succeeded` significa que el despliegue se **aplicó**; no dice que los pods nuevos estén sanos. Por eso el script también lee `workloads[].status` de la misma lista y exige que todos estén en `running`.

**Revertir es desplegar la imagen anterior.** El script lee `running_image` antes de cambiar nada y, si el despliegue nuevo falla, vuelve a guardar esa imagen y la despliega. Con `ROLLBACK=0` se desactiva. Un despliegue se rechaza con `409` mientras una restauración, un cambio de tamaño u otro despliegue tiene el servicio, y la respuesta dice cuál y desde cuándo.

Zaroz no vigila un registro por si una etiqueta se mueve (`watch_tag` siempre es `false`), así que una canalización que sube `latest` y no despliega nada nunca actualizará. Usa una etiqueta única por commit, como hacen las guías.

## Imágenes privadas

Si la imagen es privada, añade un token de registro **de solo lectura** y pásalo como `REGISTRY_USERNAME` y `REGISTRY_PASSWORD`. Zaroz lo guarda, nunca lo devuelve y lo usa para descargar desde dentro del clúster. No debe ser el token que usa tu CI para subir.

## Repositorios de ejemplo

Cada ejemplo trae un flujo de despliegue listo para los tres sistemas de CI y se ejecuta de punta a punta contra un proyecto tuyo.

| Repositorio | Qué muestra |
|---|---|
| `example-scaled-api` | una API HTTP con de 1 a 8 unidades autoescaladas detrás de un endpoint |
| `example-queue-worker` | trabajadores que escalan con una cola |
| `example-game-network` | hub, lobbies, duelos y parcelas (Minecraft), fase 2 |
| `example-job-runner` | una unidad por trabajo, iniciada por clave, resultado por evento |

Viven en la organización `zaroz-oss`, en `https://git.zaroz.cloud/zaroz-oss/`.
