---
title: "Deploy a granted GitHub repository on an owned node"
method: POST
path: "/api/github/nodes/{node_id}/deploy"
tags: ["github"]
---

# Deploy a granted GitHub repository on an owned node

`POST /api/github/nodes/{node_id}/deploy`

## Request

- Base URL: `https://app.nerdit.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://app.nerdit.ai/api/github/nodes/{node_id}/deploy`
- Auth: none declared

Picker row → token push → ``deploy_git`` → ``auto_deploy`` (D-GH-C6).

The order is the load-bearing part. The repository is added to the
node's picker first, in its own committed transaction, so the token
the pusher mints next covers it; the push is *synchronous* — the
route waits for the daemon to hold the token — because the clone
that follows resolves ``${github.installation}`` on the node and
would fail with "no token for this repository" a moment earlier. Only
then is the daemon asked to clone and register the app, with the
clone URL built here from the granted ``owner/name`` (never from the
request's spelling) and the reference name rather than a credential.
``auto_deploy`` is written last, and a failure there is reported in
the view rather than raised: the app exists by then.

A daemon without the token route (pre-P33) cannot resolve the
reference at all, so that is a 409 naming the upgrade rather than a
clone that fails with a hint about secrets. Inactive or suspended
accounts are refused with ``ENTITLEMENT_REQUIRED`` (403).

## Path parameters

- `node_id` string, uuid, required

## Request body

- GitHubDeployRequest: Deploy ``repo`` on a node from the console (D-GH-C6 "Deploy from GitHub"). ``repo`` is ``owner/name`` of a repository one of this account's *live* installations grants; ``name`` is the service name the daemon will use (its own DNS-label grammar); ``ref`` is a branch or tag, the repository's default branch when omitted; ``auto_deploy`` defaults **on** (daemon plan §6 Q1): a push to ``ref`` redeploys the app. "Add service": ``project_id`` + ``service`` (both or neither) deploy into an existing owned project instead of naming a new one; the machine labels the service ``<service>--<project>`` and ``name`` is not sent. A joined row refuses ``auto_deploy``, so asking for it is a 422 here.
  - `repo` string, required
  - `name` string, nullable
  - `ref` string, nullable
  - `auto_deploy` boolean
  - `port` integer, nullable
  - `build_settings` GitHubBuildSettings: Explicit overrides only; null resets a field and false disables the build.
    - `preset` 'node' | 'nextjs' | 'python' | 'dockerfile', nullable
    - `install` string, nullable
    - `build` union
      - string
      - false
    - `start` string, nullable
    - `node_version` string, nullable
    - `package_manager` string, nullable
    - `subdir` string, nullable
    - `public_env` object, nullable
  - `project_id` string, nullable
  - `service` string, nullable

## Response `201`

Successful Response

- GitHubDeployView: What "Deploy from GitHub" did, step by step, and what the daemon said next. ``picked`` is whether the repository was *added* to the node's picker by this request (``False`` when it was already picked); ``token`` is the push outcome for the repository's installation (``applied`` or ``unchanged`` — anything else is an error, not a view); ``auto_deploy_applied`` is whether the ``[deploy] auto_deploy`` write landed — the deploy itself is already registered when it fails, so the view says so rather than failing. ``summary``/``hints``/``next_step`` are the daemon's own, passed through the way its CLI and MCP clients show them. ``project_id`` is the daemon's own project id (``prj_…``), read off its 201; None from a daemon that predates the project noun.
  - `operation` ServiceOperationView: Dated operation metadata; arbitrary errors and output never enter this view.
    - `identity` string, nullable
    - `phase` string, nullable
    - `action` string, nullable
    - `started_at` string, date-time, nullable
    - `updated_at` string, date-time, nullable
    - `failure` string, nullable
  - `node_id` string, uuid, required
  - `service_name` string, required
  - `project_id` string, nullable
  - `installation_id` integer, required
  - `repo_full_name` string, required
  - `ref` string, nullable, required
  - `auto_deploy` boolean, required
  - `auto_deploy_applied` boolean, required
  - `picked` boolean, required
  - `token` 'applied' | 'unchanged', required
  - `summary` object, nullable, required
  - `hints` string[], required
  - `next_step` GitHubDeployNextStep, required: The daemon's machine-shaped "what now" (``{tool, args, why}``).
    - `tool` string, required
    - `args` object, required
    - `why` string, required

## Other responses

- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not Found
- `409`: Conflict
- `422`: Unprocessable Content
- `502`: Bad Gateway

---

[API](https://skmtc.dev/nerdit/apis/nerdit-cloud-web.md) · [All operations](https://skmtc.dev/nerdit/apis/nerdit-cloud-web/llms.txt) · [OpenAPI document](https://skmtc.dev/nerdit/apis/nerdit-cloud-web/revisions/06e3b44b58be?raw)
