diff --git a/README.md b/README.md index a2b0100..cc9e6d5 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ A Claude Code skill for analyzing codebases and generating everything needed to deploy them on [N0](https://nzero.pro) — a self-hosted team platform with per-workspace app hosting. +> **This repository is the single source of truth for the n0-app skill.** Copies shipped inside N0 agent sandboxes (`/workspace/skills/n0-app/SKILL.md`) and local installs are mirrors of this repo — contribute changes here. + ## What it does When triggered, this skill: 1. **Analyzes** a repository to detect the tech stack, framework, ports, databases, and dependencies -2. **Decides the image strategy**: use upstream pre-built images OR generate a custom `Dockerfile` +2. **Decides the image strategy**: upstream pre-built images, a custom `Dockerfile`, OR a zero-build `config_files` app (no CI needed) 3. **Generates** an `n0-app.json` manifest describing all services 4. **Generates** a `.gitea/workflows/build-and-push.yml` CI workflow for building and pushing Docker images 5. **Validates** the output against N0's manifest schema diff --git a/SKILL.md b/SKILL.md index 63d8b3b..4f0f719 100644 --- a/SKILL.md +++ b/SKILL.md @@ -10,6 +10,12 @@ user-invocable: true argument-hint: "[optional: path to repo or description of the app]" --- + + # N0 App Skill Analyze a codebase and generate everything needed to deploy it as a hosted app on N0. @@ -104,7 +110,7 @@ git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/ori ### Step 2: Decide the Image Strategy -**Choose ONE of these two paths:** +**Choose ONE of these three paths:** #### Path A: Use Upstream Pre-built Images (preferred when available) @@ -144,6 +150,52 @@ Use this path when: Continue to Step 2b below. +#### Path C: Zero-Build App (config_files — fastest iteration, no CI) + +Use this path when: +- The app is a small/medium single-service app (Node/Python script + static assets) +- You want the fastest possible edit → deploy loop (no Docker build, no registry, no CI wait) +- Total source size fits comfortably under **1 MiB** (Kubernetes ConfigMap limit — includes JSON overhead; keep the embedded payload under ~900 KB) + +**How it works:** use a stock public image (e.g. `node:20-alpine`, `python:3.12-alpine`) and embed the entire app source in `config_files` on a volume. The platform writes the files into the volume before the container starts, and `command` runs them directly: + +```json +{ + "services": { + "web": { + "image": "node:20-alpine", + "port": 3000, + "command": ["node", "/app/server.js"], + "volumes": { + "app": {"mount": "/app"}, + "data": {"mount": "/data", "persistent": true} + }, + "config_files": { + "app": { + "server.js": "...entire file contents...", + "index.html": "...entire file contents..." + } + } + } + } +} +``` + +Key points: +- **No Dockerfile, no Gitea Actions workflow, no registry** — skip Steps 2b and 5 entirely +- Store app state in a **persistent volume** (e.g. `/data/db.json`) — the config_files volume is recreated on every deploy +- Binary assets (images, audio) can be embedded as base64 strings and decoded by the server at startup +- The deploy loop is: edit source files → **regenerate `n0-app.json`** (script that reads the files and patches `services.web.config_files.app`) → push both to Gitea → re-import the definition (`POST .../apps/definitions/` with `{"repo_url": ...}`) → `POST .../apps/{id}/redeploy` with `{}` +- **Never hand-edit the embedded copies inside n0-app.json** — always regenerate from the real source files, e.g.: + ```python + import json + m = json.load(open("n0-app.json")) + cf = m["services"]["web"]["config_files"]["app"] + for f in ("server.js", "index.html"): + cf[f] = open(f).read() + json.dump(m, open("n0-app.json", "w"), indent=2) + ``` + ### Step 2b: Generate the Dockerfile Create a multi-stage Dockerfile optimized for the detected stack. @@ -1443,9 +1495,13 @@ After generating the manifest and pushing code to Gitea, the app must be **impor ### Prerequisites -1. **Workspace member JWT token** — any workspace member can import and deploy apps +1. **Workspace member JWT token or Personal Access Token** — any workspace member can import and deploy apps 2. **Code + n0-app.json pushed to Gitea** — the repo must exist in the workspace's Gitea instance -3. **Gitea Actions build completed** — the Docker image must be in the registry before deploy +3. **Gitea Actions build completed** — the Docker image must be in the registry before deploy (not needed for Path C zero-build apps) + +**API response envelope:** all N0 API responses are wrapped as `{"success": true, "data": {...}}` — read fields from `data`, not the top level. + +**Verifying a deployed (workspace-gated) app from the CLI:** apps behind SSO can be curled by appending an iframe token: `TOK=$(curl -s -H "Authorization: Bearer $TOKEN" "https://app.../api/v1/apps/iframe-token" | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['token'])")` then `curl "https://my-app.apps.../?_ppauth=$TOK"`. Add a cache-buster query param when verifying fresh deploys. ### Step 1: Import App Definition @@ -1493,11 +1549,30 @@ The deploy is async — a background Huey task: ### Step 3: Redeploy (after code changes) +For zero-build (config_files) apps, **re-import the definition first** so the platform picks up the new `n0-app.json` from Gitea, then redeploy: + ```bash +# 1. Re-import (refreshes AppDefinition.manifest + source commit) +curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/definitions/" \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{"repo_url": "clovrlabs/my-app"}' + +# 2. Redeploy the running instance curl -s -X POST "https://app.privateprompt.tech/api/v1/workspaces/${WS_ID}/apps/${APP_ID}/redeploy" \ - -H "Authorization: Bearer $TOKEN" + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' ``` +### Deploy Versioning & Rollback + +**Every deploy gets a unique version tag** — never rely on `latest` as a version identifier: + +- The platform tags each deploy record with the app definition's **source commit short-sha** (or a `vYYYYMMDD-HHMMSS` timestamp when no commit is available) and stores a **full manifest snapshot** on the deploy record +- The Studio UI shows the deployed version and the per-deploy tags in Deploy History +- **Rollback**: `POST .../apps/${APP_ID}/redeploy` with `{"record_id": ""}` restores that record's manifest snapshot (config_files, images, env) and redeploys it — a true rollback, not just a re-pull +- Only pass `{"image_tag": "..."}` when you explicitly want to deploy a different **image** tag of a custom-built image; version labels are NOT image tags + +Practical rule for zero-build apps: since the "version" lives in the manifest (ConfigMaps), always push a Gitea commit per change and re-import before redeploying — that commit sha becomes the deploy's version tag and enables rollback. + ### Other Operations ```bash