
Sealos Deploy
- 68 installs
- 1 repo stars
- Updated June 18, 2026
- zjy365/sealos-skills
Deploys any GitHub project or local repo to Sealos Cloud in one command, assessing readiness, generating a Dockerfile, building the image, and creating a Sealos template.
About
Runs a fully automated pipeline from source code to a running app on Sealos Cloud, with kubeconfig safety rules and confirmation gates for installs and deletions. A developer uses it for one-click deployment of a GitHub URL or local project to the cloud.
- End-to-end: readiness, Dockerfile, image build, template, and deploy
- kubectl delete and Instance-CR cleanup require explicit user confirmation
Sealos Deploy by the numbers
- 68 all-time installs (skills.sh)
- Ranked #622 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zjy365/sealos-skills --skill sealos-deployAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 68 |
|---|---|
| repo stars | ★ 1 |
| Last updated | June 18, 2026 |
| Repository | zjy365/sealos-skills ↗ |
What it does
Deploys any GitHub project or local repo to Sealos Cloud in one command, assessing readiness, generating a Dockerfile, building the image, and creating a Sealos template.
Files
Sealos Deploy
Compatibility
Sealos auth/workspace are required for deploys. Docker, buildx, and gh CLI are required only when the selected path needs local build/push. git is required when cloning from a GitHub URL or when git metadata is needed. Node.js 18+ and Python 3.8+ remain optional accelerators.
Deploy any GitHub project to Sealos Cloud — from source code to running application, one command.
kubectl Safety Rules (all phases)
All kubectl commands MUST use the Sealos kubeconfig:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verifySystem tool installation requires user confirmation. If docker, gh, or kubectl is missing and the skill can install it for the current platform, ask first and only run the install command after the user explicitly replies y.
`kubectl delete` requires user confirmation. Before deleting any resource (deployment, service, ingress, PVC, database, etc.), always ask:
WARNING: About to delete <resource kind>/<resource name>. This data cannot be recovered. Confirm? (y/n)Only proceed after user confirms. This applies even if the pipeline logic suggests deletion — always ask first.
Template API cleanup must include Instance CRs. Deployments created through scripts/deploy-template.mjs create instances.app.sealos.io/<app-name> in addition to App/workload resources. A cleanup is incomplete until instances.app.sealos.io, apps.app.sealos.io, workloads, Services, Ingresses, PVCs, and Pods are all checked.
Use this check when cleaning Template API test deployments:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" \
get instances.app.sealos.io,app,statefulset,deployment,svc,ingress,pvc,pod | grep "$APP"Delete in this order after confirmation:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete instances.app.sealos.io "$APP" --ignore-not-found --wait=false
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete app "$APP" --ignore-not-found --wait=false
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete statefulset "$APP" --ignore-not-found --wait=false
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete deployment "$APP" --ignore-not-found --wait=false
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete ingress "$APP" --ignore-not-found --wait=false
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete svc "$APP" --ignore-not-found --wait=false
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" get pvc -o name | grep "$APP" | while read -r PVC; do
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify -n "$NS" delete "$PVC" --ignore-not-found --wait=false
doneAnti-example: do not report cleanup complete after only checking app,statefulset,svc,ingress,pvc,pod; that misses instances.app.sealos.io/<app-name> and leaves the Sealos Instance layer dirty.
Usage
/sealos-deploy <github-url>
/sealos-deploy # deploy current project
/sealos-deploy <local-path>Quick Start
Execute the modules in order:
1. modules/preflight.md — Environment checks & Sealos auth 2. modules/pipeline.md — Full deployment pipeline (Phase 1–6)
Logging
Every run MUST write a log file at ~/.sealos/logs/deploy-<YYYYMMDD-HHmmss>.log.
At the very start of execution, create the log file once:
mkdir -p ~/.sealos/logs
LOG_FILE=~/.sealos/logs/deploy-$(date +%Y%m%d-%H%M%S).log
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy started" > "$LOG_FILE"Important: create the log file ONLY ONCE at the start. All subsequent writes MUST append (`>>`) to this same `$LOG_FILE`. Do NOT create a second log file.
At each phase boundary, append a log entry to the same file with Bash >>:
[2026-03-05 14:30:01] === Phase 0: Preflight ===
[2026-03-05 14:30:01] Docker: ✓ 27.5.1
[2026-03-05 14:30:01] Node.js: ✓ 22.12.0
[2026-03-05 14:30:02] Sealos auth: ✓ (region: <REGION from config.json>)
[2026-03-05 14:30:02] Project: /Users/dev/myapp (github: https://github.com/owner/repo)
[2026-03-05 14:30:03] === Phase 1: Assess ===
[2026-03-05 14:30:03] Score: 9/12 (good)
[2026-03-05 14:30:03] Language: python, Framework: fastapi, Port: 8000
[2026-03-05 14:30:03] Decision: CONTINUE
[2026-03-05 14:30:04] === Phase 2: Detect Image ===
[2026-03-05 14:30:05] Docker Hub: owner/repo:latest (arm64 only, no amd64)
[2026-03-05 14:30:05] GHCR: not found
[2026-03-05 14:30:05] Decision: no amd64 image → continue to Phase 3
[2026-03-05 14:30:06] === Phase 3: Dockerfile ===
[2026-03-05 14:30:06] Existing Dockerfile: none
[2026-03-05 14:30:07] Generated: python-fastapi template, port 8000
[2026-03-05 14:30:08] === Phase 4: Build & Push ===
[2026-03-05 14:30:08] Registry: ghcr (auto-detected via gh CLI)
[2026-03-05 14:30:30] Build: ✓ ghcr.io/zhujingyang/repo:20260305-143022
[2026-03-05 14:30:32] GHCR pullability: private package detected — deploy will auto-create image pull Secret from gh CLI
[2026-03-05 14:30:33] IMAGE_REF=ghcr.io/zhujingyang/repo:20260305-143022
[2026-03-05 14:30:34] === Phase 5: Template ===
[2026-03-05 14:30:35] Output: .sealos/template/index.yaml
[2026-03-05 14:30:36] === Phase 6: Deploy ===
[2026-03-05 14:30:36] Deploy URL: https://template.gzg.sealos.run/api/v2alpha/templates/raw
[2026-03-05 14:30:38] Status: 201 — deployed successfully
[2026-03-05 14:30:38] === DONE ===On error, log the error details before stopping:
[2026-03-05 14:30:10] === ERROR ===
[2026-03-05 14:30:10] Phase: 4 (Build & Push)
[2026-03-05 14:30:10] Error: docker buildx build failed — "npm ERR! Missing script: build"
[2026-03-05 14:30:10] Retry: 1/3At the very end, tell the user where the log is:
Log saved to: ~/.sealos/logs/deploy-20260305-143001.logScripts
Located in scripts/ within this skill directory (<SKILL_DIR>/scripts/):
| Script | Usage | Purpose |
|---|---|---|
score-model.mjs | node score-model.mjs <repo-dir> | Deterministic readiness scoring (0-12) |
validate-artifacts.mjs | node validate-artifacts.mjs --dir <work-dir> | Validate .sealos JSON artifacts against enforced schemas |
detect-image.mjs | node detect-image.mjs <github-url> [work-dir] or node detect-image.mjs <work-dir> | Detect existing Docker/GHCR images |
build-push.mjs | `node build-push.mjs <work-dir> <repo> [--registry ghcr\ | dockerhub] [--user <user>]` |
ensure-image-pull-secret.mjs | node ensure-image-pull-secret.mjs <namespace> <secret-name> <image-ref> [deployment-name] | Create/update app-scoped GHCR pull Secret and optionally patch an existing Deployment to reference it |
gh-refresh-scopes.mjs | node gh-refresh-scopes.mjs write:packages | Refresh GHCR package access in the current TTY; write:packages is sufficient for both push and private pull in this workflow |
deploy-template.mjs | `node deploy-template.mjs <template-path> [--dry-run] [--args-json '{"KEY":"value"}'\ | --args-file <file>]` |
sealos-footprint.mjs | node sealos-footprint.mjs --namespace <ns> --app <app> | Read-only inventory of Instance/App/workloads/Jobs/KubeBlocks/PVCs for deploy debug and cleanup planning |
sealos-live-smoke.mjs | node sealos-live-smoke.mjs --url <url> [--captcha-path <path>] [--login-path <path>] [--username <user>] [--password <pass>] [--auth-path <path>] | Read-only or credentialed HTTP smoke test for the real Sealos App entry URL |
sealos-auth.mjs | `node sealos-auth.mjs check\ | login\ |
All scripts output JSON. Run via Bash and parse the result.
Internal Skill Dependencies
This skill references knowledge files from co-installed internal skills. These are not user-facing — they are loaded on-demand during specific phases.
<SKILL_DIR> refers to the directory containing this SKILL.md. Sibling skills are at <SKILL_DIR>/../:
<SKILL_DIR>/../
├── sealos-deploy/ ← this skill (user entry point) = <SKILL_DIR>
├── dockerfile-skill/ ← Phase 3: Dockerfile generation knowledge
├── cloud-native-readiness/ ← Phase 1: assessment criteria
└── docker-to-sealos/ ← Phase 5: Sealos template rulesPaths used in pipeline.md follow the pattern:
<SKILL_DIR>/../dockerfile-skill/knowledge/error-patterns.md
<SKILL_DIR>/../dockerfile-skill/templates/<lang>.dockerfile
<SKILL_DIR>/../docker-to-sealos/references/sealos-specs.mdPhase Overview
| Phase | Action | Skip When |
|---|---|---|
| 0 — Preflight | Capability scan, path-specific warnings, Sealos auth | Initial blockers resolved |
| 1 — Assess | Clone repo (or use current project), analyze deployability | Score too low → stop |
| 2 — Detect | Find existing image (Docker Hub / GHCR / README) | Found → jump to Phase 5 |
| 3 — Dockerfile | Generate Dockerfile if missing | Already has one → skip |
| 4 — Build & Push | docker buildx → GHCR (auto via gh CLI) or Docker Hub (fallback) | — |
| 5 — Template | Generate Sealos application template | — |
| 5.5 — Configure | Guide user through app env vars and inputs | No inputs needed |
| 6 — Deploy | Deploy template to Sealos Cloud | — |
| 6.5 — Runtime Truth Pass | Verify the actual Sealos runtime, logs, App URL, login path, and resource footprint | User explicitly requests deploy-only output |
Decision Flow
Input (GitHub URL / local path)
│
▼
[Phase 0] Preflight ── fail → guide user to fix and STOP
│ pass
▼
[Phase 1] Assess ── not suitable → STOP with reason
│ suitable
▼
[Phase 2] Detect existing image
│
├── found (amd64) ────────────────────┐
│ │
▼ │
[Phase 3] Dockerfile (generate/reuse) │
│ │
▼ │
[Phase 4] Build & Push to registry │
│ │
◄─────────────────────────────────────┘
│
▼
[Phase 5] Generate Sealos Template
│
▼
[Phase 5.5] Configure ── present env vars → ask user for inputs → confirm
│
▼
[Phase 6] Deploy to Sealos Cloud ── 401 → re-auth
│ 409 → instance exists
▼
[Phase 6.5] Runtime Truth Pass ── runtime/log/login issue → debug template or runtime config
│
▼
Done — app deployed ✓Execution rule: Phase 1 must never start while Phase 0 still has unresolved entry blockers. Docker, gh, builder, and registry failures must be reported early, but only become hard blockers if the run later requires local build/push.
{
"client_id": "af993c98-d19d-4bdc-b338-79b80dc4f8bf",
"default_region": "https://usw-1.sealos.io",
"regions": [
"https://usw-1.sealos.io",
"https://gzg.sealos.run",
"https://bja.sealos.run",
"https://hzh.sealos.run"
]
}
{
"metadata": {
"skill_name": "sealos-deploy",
"skill_path": "/Users/jingyang/zjy365/demo/github-pack/seakills/skills/sealos-deploy",
"executor_model": "claude-opus-4-6",
"analyzer_model": "claude-opus-4-6",
"timestamp": "2026-03-12T06:00:00Z",
"evals_run": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
"runs_per_configuration": 1
},
"runs": [
{
"eval_id": 0,
"eval_name": "web-app-deploy",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 5,
"failed": 0,
"total": 5,
"time_seconds": 269.8,
"tokens": 64653,
"tool_calls": 48,
"errors": 0
},
"expectations": [
{"text": "identifies-nodejs", "passed": true, "evidence": "Report states 'Language: Node.js' and identifies Express.js + Socket.IO stack"},
{"text": "score-above-4", "passed": true, "evidence": "Score is 9/12 (Good), well above 4 threshold"},
{"text": "detects-external-db", "passed": true, "evidence": "Report identifies 'External database detected (PostgreSQL/MySQL/MongoDB)' and default SQLite"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with 6-dimension scoring table, signals summary, Docker readiness section"},
{"text": "creates-log-file", "passed": true, "evidence": "Deploy log file created at ~/.sealos/logs/deploy-20260312-102318.log"}
],
"notes": []
},
{
"eval_id": 0,
"eval_name": "web-app-deploy",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 0.8,
"passed": 4,
"failed": 1,
"total": 5,
"time_seconds": 161.4,
"tokens": 54511,
"tool_calls": 35,
"errors": 0
},
"expectations": [
{"text": "identifies-nodejs", "passed": true, "evidence": "Report states 'Runtime: Node.js >= 20.4.0'"},
{"text": "score-above-4", "passed": true, "evidence": "Score is 8/12"},
{"text": "detects-external-db", "passed": true, "evidence": "Identifies SQLite and MariaDB"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with scoring criteria, dimension table"},
{"text": "creates-log-file", "passed": false, "evidence": "No deploy log file created (no skill instructions)"}
],
"notes": []
},
{
"eval_id": 1,
"eval_name": "cli-tool-reject",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 0.75,
"passed": 3,
"failed": 1,
"total": 4,
"time_seconds": 256.4,
"tokens": 63069,
"tool_calls": 39,
"errors": 0
},
"expectations": [
{"text": "identifies-cli-tool", "passed": true, "evidence": "Report states 'bat is a command-line utility (CLI tool)'"},
{"text": "score-below-4", "passed": false, "evidence": "Score is 4/12 (not < 4). score-model.mjs gives Rust binaries 2/2 for scalability and startup by default"},
{"text": "recommends-stop", "passed": true, "evidence": "Decision: STOP. AI correctly overrides with CLI tool stop condition"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimension breakdown, signal detection, STOP rationale"}
],
"notes": ["score-model.mjs inflates Rust CLI tools to 4/12 by giving 2/2 for scalability and startup to all compiled binaries"]
},
{
"eval_id": 1,
"eval_name": "cli-tool-reject",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 139.9,
"tokens": 51363,
"tool_calls": 33,
"errors": 0
},
"expectations": [
{"text": "identifies-cli-tool", "passed": true, "evidence": "Report identifies 'short-lived, interactive command-line tool'"},
{"text": "score-below-4", "passed": true, "evidence": "Score is 0/12, all dimensions inapplicable"},
{"text": "recommends-stop", "passed": true, "evidence": "Recommendation: REJECT"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimension scoring, architecture analysis"}
],
"notes": []
},
{
"eval_id": 2,
"eval_name": "current-project",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 233.4,
"tokens": 55156,
"tool_calls": 37,
"errors": 0
},
"expectations": [
{"text": "identifies-nextjs", "passed": true, "evidence": "Report identifies a Next.js landing page in a subdirectory"},
{"text": "gives-score", "passed": true, "evidence": "Score is 5/12, with an adjusted estimate after inspecting the subdirectory app"},
{"text": "identifies-project-structure", "passed": true, "evidence": "Report identifies a mixed repository with skills plus a landing page"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimension table, signal detection, anti-pattern check"}
],
"notes": ["score-model.mjs scans repo root and can miss Dockerfiles in subdirectories, which underestimates readiness"]
},
{
"eval_id": 2,
"eval_name": "current-project",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 167.2,
"tokens": 35611,
"tool_calls": 45,
"errors": 0
},
"expectations": [
{"text": "identifies-nextjs", "passed": true, "evidence": "Identifies a Next.js 16 landing page in a subdirectory"},
{"text": "gives-score", "passed": true, "evidence": "Score is 9.5/12"},
{"text": "identifies-project-structure", "passed": true, "evidence": "Identifies a two-part project with skills plus a landing page"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with scoring criteria, dimension tables, Dockerfile analysis"}
],
"notes": []
},
{
"eval_id": 3,
"eval_name": "go-web-app",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 244.1,
"tokens": 56861,
"errors": 0
},
"expectations": [
{"text": "identifies-go", "passed": true, "evidence": "Report identifies Go with Gin framework"},
{"text": "score-above-4", "passed": true, "evidence": "Score 11/12 (Excellent)"},
{"text": "identifies-http-server", "passed": true, "evidence": "Identifies REST API + WebDAV on ports 5244/5245"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimensions, signals, env vars"}
],
"notes": []
},
{
"eval_id": 3,
"eval_name": "go-web-app",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 170.7,
"tokens": 57018,
"errors": 0
},
"expectations": [
{"text": "identifies-go", "passed": true, "evidence": "Identifies Go language"},
{"text": "score-above-4", "passed": true, "evidence": "Score 10/12"},
{"text": "identifies-http-server", "passed": true, "evidence": "Identifies web application with Docker images"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimensions and deployment recommendations"}
],
"notes": []
},
{
"eval_id": 4,
"eval_name": "nextjs-monorepo",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 246.5,
"tokens": 68317,
"errors": 0
},
"expectations": [
{"text": "identifies-nextjs", "passed": true, "evidence": "Identifies Next.js + Hono + React 19"},
{"text": "score-above-4", "passed": true, "evidence": "Score 12/12 (Excellent)"},
{"text": "detects-monorepo-or-workspace", "passed": true, "evidence": "Identifies pnpm monorepo structure"},
{"text": "produces-structured-report", "passed": true, "evidence": "Detailed report with infrastructure dependencies and architecture"}
],
"notes": []
},
{
"eval_id": 4,
"eval_name": "nextjs-monorepo",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 186.4,
"tokens": 69369,
"errors": 0
},
"expectations": [
{"text": "identifies-nextjs", "passed": true, "evidence": "Identifies Next.js framework"},
{"text": "score-above-4", "passed": true, "evidence": "Score 10/12"},
{"text": "detects-monorepo-or-workspace", "passed": true, "evidence": "Identifies complex build with multiple external services"},
{"text": "produces-structured-report", "passed": true, "evidence": "Detailed report with dimensions and deployment analysis"}
],
"notes": []
},
{
"eval_id": 5,
"eval_name": "electron-desktop",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 210.3,
"tokens": 50101,
"errors": 0
},
"expectations": [
{"text": "identifies-desktop-app", "passed": true, "evidence": "Identifies Electron desktop GUI app with BrowserWindow, ipcMain, dialog"},
{"text": "score-below-4", "passed": true, "evidence": "Score 1/12 (Poor), below threshold"},
{"text": "recommends-stop", "passed": true, "evidence": "Decision: STOP, score below 4"},
{"text": "produces-structured-report", "passed": true, "evidence": "Report with scoring, disqualifying patterns, alternative recommendation"}
],
"notes": []
},
{
"eval_id": 5,
"eval_name": "electron-desktop",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 155.6,
"tokens": 37042,
"errors": 0
},
"expectations": [
{"text": "identifies-desktop-app", "passed": true, "evidence": "Identifies Electron desktop GUI application"},
{"text": "score-below-4", "passed": true, "evidence": "Score 1/12"},
{"text": "recommends-stop", "passed": true, "evidence": "NOT DEPLOYABLE"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured assessment report"}
],
"notes": []
},
{
"eval_id": 6,
"eval_name": "rust-web-docker",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 219.9,
"tokens": 71335,
"errors": 0
},
"expectations": [
{"text": "identifies-rust", "passed": true, "evidence": "Identifies Rust with Rocket web framework"},
{"text": "score-above-4", "passed": true, "evidence": "Score 12/12 (Excellent, maximum)"},
{"text": "identifies-web-service", "passed": true, "evidence": "Identifies REST API on port 80 with /alive health check"},
{"text": "produces-structured-report", "passed": true, "evidence": "Detailed report with architecture analysis and env vars"}
],
"notes": []
},
{
"eval_id": 6,
"eval_name": "rust-web-docker",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 154.3,
"tokens": 55049,
"errors": 0
},
"expectations": [
{"text": "identifies-rust", "passed": true, "evidence": "Identifies Rust language"},
{"text": "score-above-4", "passed": true, "evidence": "Score 12/12 (Excellent)"},
{"text": "identifies-web-service", "passed": true, "evidence": "Identifies REST API with health checks"},
{"text": "produces-structured-report", "passed": true, "evidence": "Comprehensive assessment report"}
],
"notes": []
},
{
"eval_id": 7,
"eval_name": "python-library",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 214.9,
"tokens": 52427,
"errors": 0
},
"expectations": [
{"text": "identifies-library", "passed": true, "evidence": "Identifies Flask as Python library/framework published to PyPI, not deployable service"},
{"text": "score-below-7", "passed": true, "evidence": "Score 6/12 (Fair) from script, AI correctly identifies as library"},
{"text": "explains-not-standalone", "passed": true, "evidence": "Explains repository is framework source code with no application entry point"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimensions and STOP recommendation"}
],
"notes": ["score-model.mjs gives Flask framework source code 6/12 (false positive). AI assessment layer correctly overrides this and applies STOP condition."]
},
{
"eval_id": 7,
"eval_name": "python-library",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 160.9,
"tokens": 59630,
"errors": 0
},
"expectations": [
{"text": "identifies-library", "passed": true, "evidence": "Identifies Flask as web framework (library), not standalone deployable application"},
{"text": "score-below-7", "passed": true, "evidence": "Score 2/12 (Poor)"},
{"text": "explains-not-standalone", "passed": true, "evidence": "Explains containerizing this repository would be meaningless"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with dimensions"}
],
"notes": []
},
{
"eval_id": 8,
"eval_name": "python-fastapi",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 332.3,
"tokens": 79303,
"errors": 0
},
"expectations": [
{"text": "identifies-python", "passed": true, "evidence": "Identifies Python with FastAPI framework"},
{"text": "score-above-4", "passed": true, "evidence": "Raw 5/12, AI corrected to 10/12 (docker/ subdir issue)"},
{"text": "detects-external-db", "passed": true, "evidence": "Detects PostgreSQL support via psycopg2-binary"},
{"text": "produces-structured-report", "passed": true, "evidence": "Full report with scoring corrections and env var classification"}
],
"notes": ["score-model.mjs misses Dockerfile in docker/ subdirectory, scoring 5/12 raw. AI assessment layer detects this limitation and corrects to 10/12."]
},
{
"eval_id": 8,
"eval_name": "python-fastapi",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 175.0,
"tokens": 58344,
"errors": 0
},
"expectations": [
{"text": "identifies-python", "passed": true, "evidence": "Identifies Python FastAPI application"},
{"text": "score-above-4", "passed": true, "evidence": "Score 11/12"},
{"text": "detects-external-db", "passed": true, "evidence": "Identifies PostgreSQL database dependency"},
{"text": "produces-structured-report", "passed": true, "evidence": "Detailed assessment with Docker analysis"}
],
"notes": []
},
{
"eval_id": 9,
"eval_name": "java-springboot",
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 202.6,
"tokens": 54979,
"errors": 0
},
"expectations": [
{"text": "identifies-java", "passed": true, "evidence": "Identifies Java 17+, Spring Boot 4.0.3"},
{"text": "score-above-4", "passed": true, "evidence": "Score 7/12 (Good)"},
{"text": "identifies-web-service", "passed": true, "evidence": "Identifies web app on port 8080 with Actuator endpoints"},
{"text": "produces-structured-report", "passed": true, "evidence": "Structured report with scoring and env var classification"}
],
"notes": []
},
{
"eval_id": 9,
"eval_name": "java-springboot",
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 4,
"failed": 0,
"total": 4,
"time_seconds": 145.7,
"tokens": 44745,
"errors": 0
},
"expectations": [
{"text": "identifies-java", "passed": true, "evidence": "Identifies Java Spring Boot"},
{"text": "score-above-4", "passed": true, "evidence": "Score 10/12"},
{"text": "identifies-web-service", "passed": true, "evidence": "Standard HTTP on port 8080"},
{"text": "produces-structured-report", "passed": true, "evidence": "Detailed report with K8s readiness analysis"}
],
"notes": []
}
],
"run_summary": {
"with_skill": {
"pass_rate": {"mean": 0.975, "stddev": 0.075, "min": 0.75, "max": 1.0},
"time_seconds": {"mean": 243.0, "stddev": 36.1, "min": 202.6, "max": 332.3},
"tokens": {"mean": 61620, "stddev": 8882, "min": 50101, "max": 79303}
},
"without_skill": {
"pass_rate": {"mean": 0.980, "stddev": 0.060, "min": 0.8, "max": 1.0},
"time_seconds": {"mean": 161.7, "stddev": 13.1, "min": 139.9, "max": 186.4},
"tokens": {"mean": 52268, "stddev": 9928, "min": 35611, "max": 69369}
},
"delta": {
"pass_rate": "-0.005",
"time_seconds": "+81.3",
"tokens": "+9352"
}
},
"analyst_notes": [
"DISCRIMINATING ASSERTIONS: 'creates-log-file' (eval-0) is skill-specific and always fails without_skill — intentional. 'score-below-4' (eval-1 cli-tool-reject) fails for with_skill due to score-model.mjs inflating Rust binary scores — a known limitation.",
"SCORE-MODEL.MJS BUG #1 (Compiled binaries): Gives all compiled binaries (Rust/Go) 2/2 for scalability and startup regardless of whether they run an HTTP server. This inflates bat CLI from expected 0→4/12. The AI assessment layer correctly catches this and applies STOP via CLI tool pattern detection.",
"SCORE-MODEL.MJS BUG #2 (Subdirectory blind spot): Scans repo root only. Projects with Dockerfile/package.json in subdirectories get deflated scores: mealie (docker/ → raw 5/12 vs actual 10/12), while root-level projects like spring-petclinic are unaffected. AI correctly compensates when it inspects subdirectories.",
"SCORE-MODEL.MJS BUG #3 (Library false positive): Flask framework source code scores 6/12 (above CONTINUE threshold) because it has Python files and test structure. AI correctly identifies the absence of application entry point and applies STOP. Without-skill baseline scores it a correct 2/12.",
"COST TRADEOFF: With-skill runs average 81.3s slower and use 9352 more tokens (+18%). The overhead comes from reading skill files (~30KB), running score-model.mjs, and writing structured artifacts (context.json, deploy log). The payoff is deployment-specific artifacts and consistent structured output format.",
"NON-DISCRIMINATING ASSERTIONS: Most assertions (identifies-*, score-*, produces-structured-report) pass in both configurations across 9/10 evals. The skill's differentiated value shows in: (a) structured artifact output, (b) AI score correction via assessment layer, (c) consistent STOP logic for edge cases.",
"REJECTION ACCURACY: All 3 non-deployable cases (bat CLI, drawio desktop, Flask library) correctly identified by both configurations. With-skill provides more explicit STOP rationale and structured documentation of the rejection reason.",
"NOTABLE HIGH SCORERS: lobe-chat (Next.js monorepo, 12/12), vaultwarden (Rust+Docker, 12/12), alist (Go web, 11/12) — all correctly assessed as excellent K8s candidates.",
"OVERALL: Both with and without skill pass 97.5-98% of assertions. With-skill consistently provides deployment artifacts, structured logging, and compensates for score-model.mjs blind spots. The primary iteration target should be fixing score-model.mjs subdirectory scanning."
]
}
{
"skill_name": "sealos-deploy",
"evals": [
{
"id": 0,
"prompt": "I want to deploy uptime-kuma to Sealos, GitHub: https://github.com/louislam/uptime-kuma",
"expected_output": "High readiness score (7+), identifies Node.js project with external services, recommends proceeding with deployment",
"files": [],
"assertions": [
{"name": "identifies-nodejs", "description": "Correctly identifies the project as Node.js"},
{"name": "score-above-4", "description": "Readiness score is >= 4 (deployable threshold)"},
{"name": "detects-external-db", "description": "Detects external database dependency (SQLite/MariaDB)"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report with dimensions and scores"},
{"name": "creates-log-file", "description": "Creates a deploy log file at ~/.sealos/logs/"}
]
},
{
"id": 1,
"prompt": "/sealos-deploy https://github.com/sharkdp/bat",
"expected_output": "Low readiness score (0-3), identifies as CLI tool not suitable for cloud deployment, recommends STOP",
"files": [],
"assertions": [
{"name": "identifies-cli-tool", "description": "Correctly identifies bat as a CLI tool / non-web application"},
{"name": "score-below-4", "description": "Readiness score is < 4 (not suitable for cloud deployment)"},
{"name": "recommends-stop", "description": "Explicitly recommends NOT deploying / STOP"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 2,
"prompt": "/sealos-deploy",
"expected_output": "Low readiness score (0-3), identifies the current repository as a skills pack or tooling repo rather than a deployable web service, recommends STOP",
"files": [],
"assertions": [
{"name": "identifies-skill-pack", "description": "Identifies the current repository as a skills pack or tooling repo rather than a standalone web app"},
{"name": "score-below-4", "description": "Readiness score is < 4 (not suitable for direct cloud deployment)"},
{"name": "recommends-stop", "description": "Explicitly recommends NOT deploying / STOP"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 3,
"prompt": "Help me deploy alist to Sealos, project URL: https://github.com/alist-org/alist",
"expected_output": "High readiness score (8+), identifies Go web application with REST API and WebDAV server, recommends CONTINUE",
"files": [],
"assertions": [
{"name": "identifies-go", "description": "Correctly identifies the project as Go language"},
{"name": "score-above-4", "description": "Readiness score is >= 4 (deployable threshold)"},
{"name": "identifies-http-server", "description": "Identifies this as an HTTP server / web service"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 4,
"prompt": "I want to deploy lobe-chat to Sealos: https://github.com/lobehub/lobe-chat",
"expected_output": "High readiness score (10+), identifies Next.js monorepo with pnpm workspaces, recommends CONTINUE",
"files": [],
"assertions": [
{"name": "identifies-nextjs", "description": "Correctly identifies the project as Next.js"},
{"name": "score-above-4", "description": "Readiness score is >= 4 (deployable threshold)"},
{"name": "detects-monorepo-or-workspace", "description": "Detects pnpm monorepo / workspace structure"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 5,
"prompt": "/sealos-deploy https://github.com/jgraph/drawio-desktop",
"expected_output": "Very low readiness score (0-1), identifies as Electron desktop GUI app unsuitable for cloud deployment, recommends STOP",
"files": [],
"assertions": [
{"name": "identifies-desktop-app", "description": "Correctly identifies drawio-desktop as an Electron / desktop GUI application"},
{"name": "score-below-4", "description": "Readiness score is < 4 (not suitable for cloud deployment)"},
{"name": "recommends-stop", "description": "Explicitly recommends NOT deploying / STOP"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 6,
"prompt": "Deploy vaultwarden to Sealos: https://github.com/dani-garcia/vaultwarden",
"expected_output": "Maximum readiness score (12/12), identifies Rust web application with Rocket framework and existing Dockerfile, recommends CONTINUE",
"files": [],
"assertions": [
{"name": "identifies-rust", "description": "Correctly identifies the project as Rust language"},
{"name": "score-above-4", "description": "Readiness score is >= 4 (deployable threshold)"},
{"name": "identifies-web-service", "description": "Identifies this as a web service / HTTP server"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 7,
"prompt": "/sealos-deploy https://github.com/pallets/flask",
"expected_output": "Low score or STOP, correctly identifies Flask as a Python web framework library not a deployable application",
"files": [],
"assertions": [
{"name": "identifies-library", "description": "Correctly identifies Flask as a framework/library, not a standalone deployable app"},
{"name": "score-below-7", "description": "Readiness score is < 7 (library should not be considered highly deployable)"},
{"name": "explains-not-standalone", "description": "Explains why this repository is not directly deployable as a service"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 8,
"prompt": "Help me deploy mealie (home recipe management): https://github.com/mealie-recipes/mealie",
"expected_output": "High readiness score (8+), identifies Python FastAPI application with PostgreSQL support, recommends CONTINUE",
"files": [],
"assertions": [
{"name": "identifies-python", "description": "Correctly identifies the project as Python"},
{"name": "score-above-4", "description": "Readiness score is >= 4 (deployable threshold)"},
{"name": "detects-external-db", "description": "Detects PostgreSQL database dependency"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
},
{
"id": 9,
"prompt": "/sealos-deploy https://github.com/spring-projects/spring-petclinic",
"expected_output": "Good readiness score (6+), identifies Java Spring Boot application with port 8080 and Actuator endpoints, recommends CONTINUE",
"files": [],
"assertions": [
{"name": "identifies-java", "description": "Correctly identifies the project as Java / Spring Boot"},
{"name": "score-above-4", "description": "Readiness score is >= 4 (deployable threshold)"},
{"name": "identifies-web-service", "description": "Identifies this as a web application on port 8080"},
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
]
}
]
}
Lessons Learned from Real Deployments
This document captures patterns and solutions from actual Sealos deployment experiences to prevent repeated mistakes.
---
Case Study: EverShop (Public URL + Image Detection)
Project: EverShop - Node.js e-commerce platform using node-config GitHub: evershopcommerce/evershop Issues Encountered: 2 (public URL misconfiguration, image detection miss)
Issue 1: Hardcoded localhost Base URL
- Symptom: App deployed successfully but all frontend API calls failed (404/CORS errors)
- Root Cause: App uses node-config with
getConfig('shop.homeUrl', 'http://localhost:3000')— when no config override exists, all generated URLs point to localhost - Detection Signal:
packages/evershop/src/lib/util/getBaseUrl.tscontains fallback tohttp://localhost:3000 - Fix: Created ConfigMap with
config/default.jsoncontaining{"shop":{"homeUrl":"https://<public-url>"}}, mounted viasubPathto avoid overwriting other config files - Generalized Pattern: Public URL via file-based config — many apps (especially Node.js with node-config, PHP with config files) read their public URL from config files rather than env vars. When
localhostfallback is detected in source code, a ConfigMap override is required. - Status: Pattern added to
conversion-mappings.md(Strategy B: ConfigMap)
Issue 2: Docker Hub Image Not Found
- Symptom:
detect-image.mjsreturned{ "found": false }, triggering unnecessary Docker build - Root Cause: Script only checked
<github-owner>/<github-repo>(i.e.,evershopcommerce/evershop), but official Docker image is atevershop/evershop - Detection Signal: Docker Hub namespace differs from GitHub org — common when project name is shorter than org name
- Fix: Added fallback check for
<repo-name>/<repo-name>pattern indetect-image.mjs - Other Known Examples:
- GitHub
nextcloud/server→ Docker Hubnextcloud/nextcloud - GitHub
gogs/gogs→ Docker Hubgogs/gogs(same, but org ≠ repo in other cases) - Status: Fallback added to
detect-image.mjs
Generalized Lessons
1. Public URL Detection is Critical: Always scan source code for localhost fallback patterns during Phase 5.2. Missing this causes subtle runtime failures (app loads but API calls fail). 2. Image Detection Needs Multiple Strategies: Don't assume Docker Hub namespace matches GitHub org. Check <repo>/<repo> as fallback. 3. Config File Overrides via ConfigMap: When an app uses file-based config (not env vars) for its public URL, use a ConfigMap with subPath mount to inject only the needed override without replacing the entire config directory.
---
Consolidated Patterns
KubeBlocks Redis Readiness Lag
Redis Sentinel can report readiness before the primary Redis component and the default account Secret appear. Treat final Cluster Ready/Running state, ${APP_NAME}-redis-redis-account-default, ${APP_NAME}-redis-redis-redis.${NAMESPACE}.svc.cluster.local, and successful application registration/login as the acceptance signal.
GHCR Push Succeeds but Cluster Pull Fails (Prevents ImagePullBackOff)
detection:
trigger:
- "Phase 4 built a ghcr.io/<user>/<repo>:<tag> image locally"
- "Deployment later stalls with ImagePullBackOff or ErrImagePull"
root_causes:
- "GitHub Container Registry package visibility is still private"
- "Cluster has no imagePullSecret for ghcr.io"
decision:
if_local_gh_cli_is_available:
require: "create or refresh the namespace image pull Secret automatically before deploy/update"
else:
fallback: "package must be public, or the operator must provide registry pull credentials another way"
skip_when:
- "Phase 2 reused an existing public image"
verification:
visibility_check: "gh api /user/packages/container/<repo> -q .visibility"
anonymous_pull_check: "GET ghcr token, then HEAD/GET manifest from ghcr.io/v2/.../manifests/<tag>"
fixes:
preferred: "create/update the app-scoped imagePullSecret from gh auth token during deploy"
fallback_1: "make the GHCR package public"
fallback_2: "push to Docker Hub instead"Public URL Misconfiguration (Prevents Runtime API Failures)
detection:
# Scan source code for these patterns
env_var_patterns:
- "BASE_URL"
- "SITE_URL"
- "APP_URL"
- "NEXTAUTH_URL"
- "PUBLIC_URL"
- "EXTERNAL_URL"
config_file_patterns:
- "getConfig(.*[Uu]rl"
- "homeUrl"
- "baseUrl"
- "siteUrl"
- "http://localhost"
# Decision
strategy:
env_var_supported: "Strategy A — add env var with public URL"
config_file_only: "Strategy B — create ConfigMap with minimal config override"Docker Hub Namespace Mismatch (Prevents Unnecessary Builds)
detection:
# Primary: <github-owner>/<github-repo>
primary: "${github_owner}/${github_repo}"
# Fallback 1: <repo-name>/<repo-name> (when owner ≠ repo)
fallback_repo_repo: "${github_repo}/${github_repo}"
# Fallback 2: README scan for docker pull/run references
fallback_readme: "scan README.md for image references"BillionMail Safe Entry and DB Bootstrap (Prevents access denied and Init Loops)
detection:
symptoms:
- "Pod is Running but login APIs return access denied"
- "Root URL and configured App URL behave differently in a fresh session"
- "Init container waits forever on application-specific database checks"
- "Startup logs mention pg_indexes, relay compatibility objects, or missing PostgreSQL search_path"
- "PostgreSQL bootstrap logs show syntax error at or near \"$\""
- "PostgreSQL bootstrap logs show syntax error at or near \":\" for ALTER ROLE ... :'app_password'"
runtime_entry:
final_config:
safe_path: ""
app_url: "root Sealos App URL"
main_container_working_dir: "/opt/billionmail/core"
main_container_command: "mkdir -p template && exec ./billionmail"
command_boundary:
keep_in_main_container:
- "official entrypoint or short exec wrapper only"
move_out_of_main_container:
- "file preparation and permission repair"
- "certificate/log-file setup"
- "database bootstrap and compatibility objects"
- "relay/search-path repair"
verification:
- "GET /api/get_validate_code returns success from the root App URL"
- "POST /api/login succeeds with generated admin credentials"
- "An authenticated page or API route works after login"
- "Live pod main container command stays short and ends in exec"
database_bootstrap:
principle: "Make critical compatibility objects idempotent and self-healing in init containers"
verify_live_state:
- "public.pg_indexes compatibility view exists"
- "relay compatibility objects exist"
- "application role search_path resolves expected public schema objects"
ttl_job_note: "A completed or cleaned-up Job is only historical evidence; the database state is the acceptance signal"
quoting_rules:
- "Prefer shell-level guard queries plus simple SQL over inline DO $$ blocks"
- "Use single-quoted heredocs for psql -v variable interpolation"
- "Do not put :'var' psql syntax inside psql -c strings"
generalized_pattern:
- "The Sealos App URL must be the URL that succeeds from a fresh browser session"
- "Path-based safe entrances need root-path smoke tests because launchers may normalize or revisit root"
- "Post-rollout log scans are part of acceptance for login-gated web apps"ERPNext / Frappe Admin Username (Prevents Login Smoke Mismatch)
detection:
symptoms:
- "Template exposes admin username/password inputs"
- "Login succeeds with Administrator but fails with the configured username"
- "bench new-site completed and the ready marker exists"
root_cause: "bench new-site --admin-password sets the built-in Administrator password; it does not rename the login identity"
template_contract:
administrator_inputs:
- "Declare admin_username and admin_password in spec.inputs when deployers must choose credentials"
- "Pass application admin credentials as direct env values to the Frappe init path"
- "Keep database credentials on KubeBlocks secrets"
reserved_names:
- "Administrator"
- "Guest"
recommended_default_username: "admin"
init_sequence:
- "Run bench new-site with the deploy-time admin password"
- "Set User.username for the built-in Administrator user to the deploy-time admin username"
- "Enable allow_login_using_user_name"
- "Clear Frappe cache"
- "Write the ready marker after username/login settings, migrations, and app installs finish"
runtime_truth:
- "Login smoke uses the exact admin username/password collected during deploy"
- "Password values are masked in logs, summaries, and final output"Deployment Pipeline
After preflight passes, execute Phase 1–6 in order.
SKILL_DIR refers to the directory containing this skill's SKILL.md. Sibling skills are at <SKILL_DIR>/../.
Use ENV from preflight to choose between script mode (Node.js available) and fallback mode (AI-native).
Artifact Directory
All pipeline outputs are written under .sealos/ in WORK_DIR:
<WORK_DIR>/.sealos/
├── config.json ← user configuration overrides (manual, committed to git)
├── state.json ← deployment state (auto-maintained after Phase 6)
├── analysis.json ← project analysis snapshot (regenerated each deploy)
├── build/ ← created only if Phase 4 actually runs
│ └── build-result.json ← Phase 4 result (`success` or `failed`)
└── template/
└── index.yaml ← Phase 5 Sealos templateFile responsibilities:
config.json— optional user overrides (port, base_image, build_command, etc.). Created manually by user, committed to git. All fields optional.analysis.json— project analysis snapshot written after Phase 1 (language, framework, score, etc.). Regenerated each deploy.state.json— deployment state written after Phase 6 success. Containslast_deployandhistory. Enables UPDATE mode on subsequent runs.
Note: When reading dockerfile-skill modules (analyze.md, generate.md, build-fix.md), they reference docker-build/ as their default output path. In this pipeline, always write to .sealos/build/ instead. Similarly, template output goes to .sealos/template/ instead of template/.
JSON artifacts under .sealos/ are governed by explicit schemas in <SKILL_DIR>/schemas/:
config.schema.jsonanalysis.schema.jsonbuild-result.schema.jsonstate.schema.json
Validate them with:
node "<SKILL_DIR>/scripts/validate-artifacts.mjs" --dir "$WORK_DIR"Writers should validate on write; readers should validate before trusting resume/update state.
At the very start of the pipeline (before Phase 1), create the base artifact directory:
mkdir -p "$WORK_DIR/.sealos" "$WORK_DIR/.sealos/template"Create "$WORK_DIR/.sealos/build" lazily when Phase 4 starts. If Phase 2 finds an existing image and skips Phase 4, build/ should remain absent rather than exist as an empty directory.
Read user config (if exists): If .sealos/config.json exists, read it. User-provided values take priority over auto-detection and AI inference throughout the pipeline.
{
"port": 8080,
"node_version": "20",
"start_command": "node dist/main.js",
"build_command": "pnpm build:prod",
"system_deps": ["ffmpeg"],
"base_image": "node:20-slim",
"env_overrides": { "NODE_ENV": "production" },
"skip_phases": ["assess"]
}All fields are optional. If a field is present, it overrides the corresponding auto-detected value.
Deployment Mode Detection
After preflight, determine whether this is a first deploy or an update of an existing deployment.
Step 1: Check for previous deployment state
Read .sealos/state.json in WORK_DIR. If it exists and contains a last_deploy key with app_name, proceed to Step 2.
If no last_deploy key or file doesn't exist → proceed to Step 1.5 (attempt discovery from cluster).
Step 1.5: Discover existing deployment from cluster (migration)
Projects deployed by an older version of the skill may have no last_deploy section in state.json (or no state.json at all). If ENV.kubectl is true and ~/.sealos/kubeconfig exists, attempt to discover an existing deployment by project name:
# Derive the namespace from the sealos kubeconfig
NAMESPACE=$(KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
config view --minify -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null)
# Search for a deployment whose name starts with the repo name
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get deploy -n "$NAMESPACE" \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[0].image}{"\n"}{end}' 2>/dev/null \
| grep -i "^$REPO_NAME"If a match is found (e.g., evershop-uvbp0n0n zhujingyang/evershop:20260309):
1. Query the full details to reconstruct the deployed state:
# Get the ingress host
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get ingress/<app_name> -n "$NAMESPACE" \
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null2. Present to user for confirmation:
Found an existing deployment that appears to match this project:
App: evershop-uvbp0n0n
Image: zhujingyang/evershop:20260309
URL: https://evershop-4ha6b4mh.gzg.sealos.run
Namespace: ns-qiqovyrm
Is this the deployment you want to update? (y/n)3. If user confirms → write the reconstructed last_deploy section to .sealos/state.json (create file if needed), then proceed to Step 2.
4. If user says no, or no match found → DEPLOY mode (skip to Resume Detection below).
Step 2: Verify deployment is still running (requires kubectl)
If ENV.kubectl is false:
- Inform user:
"Found previous deployment record for {app_name}, but kubectl is not available. Will create a new instance instead." - → DEPLOY mode
If ENV.kubectl is true, query the cluster:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get deployment/<app_name> -n <namespace> \
-o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null- Command fails (deployment deleted or kubeconfig expired) → DEPLOY mode (remove
.sealos/state.jsonor clearlast_deploy) - Command returns current image → proceed to Step 3
Step 3: Ask user
Present the detected state and let the user choose:
Detected existing deployment:
App: <app_name>
Image: <image>
URL: <url>
1. Update this deployment (rebuild & push new image)
2. Deploy as a new instance
Default: Update- User picks Update → UPDATE mode (jump to Update Path below)
- User picks New instance → DEPLOY mode (rename state.json to state.json.bak)
---
Resume Detection
Only applies in DEPLOY mode. Check for artifacts from a previous incomplete deploy using file existence:
| Condition | Meaning | Behavior |
|---|---|---|
.sealos/state.json has last_deploy | Already deployed | Enter UPDATE mode (handled above) |
.sealos/analysis.json exists | Phase 1 completed | Ask user: skip assessment? |
Dockerfile exists | Phase 3 completed | Skip Dockerfile generation |
.sealos/build/build-result.json exists and outcome: "success" | Phase 4 completed | Ask user: skip rebuild? |
.sealos/template/index.yaml exists | Phase 5 completed | Ask user: skip template generation? |
If any artifacts exist, report to user: "Found artifacts from a previous deploy attempt. [list found artifacts]." Ask: "Resume from where it left off? Or restart from Phase 1?"
If restart → remove .sealos/analysis.json, .sealos/build/, .sealos/template/index.yaml and start fresh.
---
Phase 1: Assess
WORK_DIR, GITHUB_URL, REPO_NAME, and README context are already resolved in preflight (Step 2). Use those directly — no need to re-derive.
1.2 Deterministic Scoring
If Node.js available:
node "<SKILL_DIR>/scripts/score-model.mjs" "$WORK_DIR"Output: { "score": N, "verdict": "...", "dimensions": {...}, "signals": {...} }
If Node.js not available (fallback): Perform the scoring yourself by reading project files and applying these rules:
1. Detect language: package.json → Node.js, go.mod → Go, requirements.txt → Python, pom.xml → Java, Cargo.toml → Rust 2. Detect framework: read dependency files for known frameworks (Next.js, Express, FastAPI, Gin, Spring Boot, etc.) 3. Check HTTP server: does the project listen on a port? 4. Check state: external DB (PostgreSQL/MySQL/MongoDB) vs local state (SQLite)? 5. Check config: .env.example exists? 6. Check Docker: Dockerfile or docker-compose.yml exists?
Score 6 dimensions (0-2 each, max 12). For detailed criteria, read: <SKILL_DIR>/../cloud-native-readiness/knowledge/scoring-criteria.md
Decision:
score < 4→ STOP. Tell user: "This project scored {N}/12 ({verdict}). Not suitable for containerized deployment because: {dimension_details for 0-score dimensions}."score >= 4→ CONTINUE.
1.3 AI Quick Assessment
Use structured signals from Phase 1.2 score-model output directly:
signals.primary_language— primary language (priority-sorted when multiple detected)signals.framework— detected frameworkssignals.package_manager— detected package manager (npm/yarn/pnpm/bun/pip/go/etc.)signals.port— detected port (from framework defaults)signals.databases— detected database types (postgres/mysql/mongodb/redis/sqlite)signals.runtime_version— runtime version with source (e.g.,{ node: "22", source: "engines" })signals.is_monorepo,signals.has_docker,signals.has_env_example
Focus AI effort on what the script cannot detect: env_vars classification, complexity_tier assessment, and port override from source code (if port_source is "unknown").
Based on the score result and your own analysis of the project, assess:
1. Read key files: README.md, package.json/go.mod/requirements.txt, Dockerfile (if exists) 2. Check: Is this a web service, API, or worker with network interface? 3. Determine: ports, required env vars, database dependencies, special concerns
If the score is borderline (4-6), also read:
<SKILL_DIR>/../cloud-native-readiness/knowledge/scoring-criteria.md— detailed rubrics<SKILL_DIR>/../cloud-native-readiness/knowledge/anti-patterns.md— disqualifying patterns
STOP conditions:
- Desktop/GUI application (Electron without server, Qt, GTK)
- Mobile app without backend
- CLI tool / library / SDK (no network service)
- No identifiable entry point or build system
Record for later phases: language, framework, ports, env_vars, databases, has_dockerfile
Env var classification (for Phase 5.5 interactive configuration): When recording env_vars, also classify each one:
auto— can be auto-generated (random secrets, internal URLs, DB connections)required— user must provide (external API keys, admin email, SMTP, OAuth)optional— has sensible default, user may customize (log level, feature flags)
Sources for env var detection:
.env.exampleor.env.sample— most reliable source of required env varsdocker-compose.ymlenvironment:section- README sections about configuration/environment
- Source code imports of
process.env.*oros.environ[]
Write analysis.json
After Phase 1 completes, write .sealos/analysis.json with the full analysis snapshot:
{
"generated_at": "<ISO timestamp>",
"project": {
"github_url": "<GITHUB_URL>",
"work_dir": "<WORK_DIR>",
"repo_name": "<REPO_NAME>",
"branch": "<BRANCH or null>"
},
"score": { "total": "<N>", "verdict": "<verdict>", "dimensions": {} },
"language": "<signals.primary_language>",
"all_languages": ["<all detected languages from signals.language>"],
"framework": "<detected framework>",
"package_manager": "<npm|yarn|pnpm|bun|pip|go|cargo|maven|gradle>",
"port": "<primary port>",
"databases": ["<detected database types>"],
"runtime_version": { "<language>": "<major version>", "source": "<detection source>" },
"env_vars": {},
"has_dockerfile": false,
"complexity_tier": "<L1|L2|L3>",
"image_ref": null
}If .sealos/config.json exists, apply user overrides: e.g., if config.json has "port": 8080, use that instead of the auto-detected value. Priority: user config > script detection > AI inference.
The image_ref field is set to null initially. It will be filled in Phase 2 (if existing image found) or Phase 4 (after build).
Present Analysis Summary
After writing .sealos/analysis.json, present a concise repository analysis summary to the user. This summary should expose only the key conclusions, not the full artifact contents.
Recommended format:
Repository Analysis:
- Type: <web app | api | worker | cli | library>
- Language: <language>
- Framework: <framework or "none detected">
- Port: <port or "not detected">
- Database: <postgres/mysql/redis/... or "none detected">
- Dockerfile: <yes/no>
- Score: <N>/12 (<verdict>)
- Decision: <continue | stop>Output rules:
- Keep the summary short and decision-oriented
- Do not dump the full
env_varsobject or dimension-by-dimension internals unless the user asks - Do not add a default "full details" block after this summary
- If the assessment stops the pipeline, briefly state the top blocker(s)
- If the assessment continues, state the next phase in one short line
---
Phase 2: Detect Existing Image
If Node.js available:
# With GitHub URL:
node "<SKILL_DIR>/scripts/detect-image.mjs" "$GITHUB_URL" "$WORK_DIR"
# Local project without GitHub URL:
node "<SKILL_DIR>/scripts/detect-image.mjs" "$WORK_DIR"The script auto-detects GitHub URL from git remote if only a directory is given.
Output: { "found": true, "image": "...", "tag": "...", ... } or { "found": false }
If Node.js not available (fallback — use curl):
1. Parse owner/repo from GITHUB_URL (if empty, try git -C "$WORK_DIR" remote get-url origin) 2. If still no GitHub URL, skip Docker Hub / GHCR checks and only scan project files for image references 3. Docker Hub check (try <owner>/<repo>, then <repo>/<repo> if different):
curl -sf "https://hub.docker.com/v2/namespaces/<owner>/repositories/<repo>/tags?page_size=10"
# If not found and owner != repo:
curl -sf "https://hub.docker.com/v2/namespaces/<repo>/repositories/<repo>/tags?page_size=10"4. GHCR check:
TOKEN=$(curl -sf "https://ghcr.io/token?scope=repository:<owner>/<repo>:pull" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
curl -sf -H "Authorization: Bearer $TOKEN" "https://ghcr.io/v2/<owner>/<repo>/tags/list"5. docker-compose.yml scan — AI reads docker-compose.yml / docker-compose.yaml (already in Phase 1 context) and extracts image: fields. Exclude infrastructure images (postgres, mysql, redis, mongo, etc.). For each candidate, verify with curl against Docker Hub or GHCR. 6. CI workflow scan — AI reads .github/workflows/*.yml and extracts docker push targets, images: fields, and tags: references. Verify each candidate. 7. Search README.md for ghcr.io/ references, docker run/pull commands, and hub.docker.com/r/<ns>/<repo> URLs 8. Docker Hub search API (catch-all) — if nothing found above:
curl -sf "https://hub.docker.com/v2/search/repositories/?query=<repo>&page_size=5"
# For each result, fetch detail and check if full_description mentions github.com/<owner>/<repo>
curl -sf "https://hub.docker.com/v2/repositories/<ns>/<repo>/"9. For any candidate, verify amd64: docker manifest inspect <image>:<tag>
Prefer versioned tags (v1.2.3) over latest.
Phase 2 Post-Verification (AI)
After Phase 2 produces a result, the AI should cross-validate:
1. If `source` is `dockerhub` or `ghcr` (direct owner/repo match) — high confidence, no extra validation needed. 2. If `source` is `compose`, `ci-workflow`, `dockerhub-readme`, or `dockerhub-search` — cross-check with project context:
- Does the README mention this image or its namespace?
- Does
docker-compose.ymlreference it? - Does the Docker Hub repo description link back to this GitHub project?
- If multiple signals agree → high confidence. If only one signal → note as medium confidence in your assessment.
3. If `found: false` — the AI should use its Phase 1 analysis context to attempt one more check: if Phase 1 identified a Docker image name from project docs or code that the script didn't find, try verifying it manually with curl.
Update analysis.json
If an existing image is found, update .sealos/analysis.json to set image_ref to {image}:{tag}.
Decision:
- Found amd64 image → record
IMAGE_REF = {image}:{tag}, skip to Phase 5 - Not found → continue to Phase 3
---
Phase 3: Dockerfile
3.1 Check Existing Dockerfile
If WORK_DIR/Dockerfile exists: 1. Read it and assess quality 2. Reasonable (multi-stage or appropriate for language) → use directly, go to Phase 4 3. Problematic (uses :latest, runs as root, missing essential deps) → fix, then Phase 4
3.2 Generate Dockerfile
If no Dockerfile exists, generate one.
Load the appropriate template from the internal dockerfile-skill:
<SKILL_DIR>/../dockerfile-skill/templates/golang.dockerfile
<SKILL_DIR>/../dockerfile-skill/templates/nodejs-express.dockerfile
<SKILL_DIR>/../dockerfile-skill/templates/nodejs-nextjs.dockerfile
<SKILL_DIR>/../dockerfile-skill/templates/python-fastapi.dockerfile
<SKILL_DIR>/../dockerfile-skill/templates/python-django.dockerfile
<SKILL_DIR>/../dockerfile-skill/templates/java-springboot.dockerfileRead the template matching the detected language/framework, then adapt it:
- Replace placeholder ports with detected ports
- Adjust build commands based on actual package manager (npm/yarn/pnpm/bun)
- Add system dependencies if needed
- Set correct entry point
Pre-load Phase 1 analysis for analyze.md:
Read .sealos/analysis.json before running analyze.md. The following fields are available as pre-loaded context, so analyze.md can skip its overlapping detection steps: language, framework, package_manager, port, databases, has_dockerfile, complexity_tier.
For detailed analysis guidance, read:
<SKILL_DIR>/../dockerfile-skill/modules/analyze.md — 17-step analysis process
<SKILL_DIR>/../dockerfile-skill/modules/generate.md — generation rules and best practicesValidate generated Dockerfile:
After generating the Dockerfile, run validation if Node.js is available:
node "<SKILL_DIR>/../dockerfile-skill/scripts/validate-dockerfile.mjs" "$WORK_DIR/Dockerfile" --port=<detected_port> --jsonIf validation reports errors, fix the Dockerfile before proceeding to Phase 4. If Node.js is not available, manually verify the Validation Checklist in generate.md.
Key Dockerfile principles:
- Multi-stage build (builder + runtime)
- Pin base image versions (never
:latest) - Run as non-root user (USER 1001)
- Proper
.dockerignore
Also generate .dockerignore:
.git
node_modules
__pycache__
.env
.env.local
*.md
.vscode
.idea
.sealos---
Phase 4: Build & Push
4.0 Choose Image Destination
Registry selection is deferred to this phase because it's only needed when building. If Phase 2 found an existing image, this phase is skipped entirely.
Before any login step, tell the user:
This app will be built locally with Docker.
Choose where to push the image:
1. GHCR (recommended) — agent can run `gh auth login` and finish browser auth with you
2. Docker Hub — public images only; use your existing `docker login` session, or run `docker login` in another terminalDefault to GHCR when the user says "either is fine".
Important:
- This choice is about the image registry only. Local builds still require Docker either way.
- If the user chooses GHCR, use
gh auth loginas the preferred interactive auth path. - If the user chooses Docker Hub, treat that path as public-image only.
- If the user chooses Docker Hub and there is no active Docker Hub session, stop and ask the user to run
docker loginin another terminal before continuing.
If the user chooses GHCR:
gh auth status 2>/dev/nullIf authenticated:
GH_USER=$(gh api user -q .login)
gh auth token | docker login ghcr.io -u "$GH_USER" --password-stdin
REGISTRY=ghcrImportant:
- Before the first GHCR push, ensure the local
ghsession haswrite:packages. - For GHCR,
write:packagesis sufficient for both pushing and later creating the app-scoped image pull Secret. GitHub CLI may not show a separateread:packagesentry even though pull access works. - If the current session is missing GHCR package access, refresh with:
node "<SKILL_DIR>/scripts/gh-refresh-scopes.mjs" write:packages
- When
build-push.mjsorensure-image-pull-secret.mjsruns inside a TTY, it will now ask once whether it should refresh missing GHCR scopes and, ony, rungh auth refreshin the same PTY before continuing. - If
gh auth refreshexits successfully but the scopes are still missing, the script will immediately fall back to a fullgh auth login --web --scopes ...in the same PTY and only continue after re-checking the scopes. - A successful GHCR push does not guarantee Sealos can pull the image.
- For private GHCR packages, keep the deployment path GHCR-first and create an image pull Secret from the local
ghCLI session before applying or updating workloads. - Do not surface raw registry host/username/password/email as user-facing template inputs when local
gh auth statusis already available.
If build-push.mjs or ensure-image-pull-secret.mjs returns:
{
"action": "gh_scope_refresh_required",
"tty_required": true,
"suggested_command": "node <SKILL_DIR>/scripts/gh-refresh-scopes.mjs write:packages"
}then the agent should: 1. Ask the user once: Missing GitHub Packages permission for GHCR. Refresh now? (y/n) 2. If the current script is already running in a PTY, answer y there and let it continue in-place 3. Otherwise run the suggested_command in the current PTY/TTY session 4. If gh prompts Press Enter to open github.com in your browser..., send Enter in the same PTY 5. After the refresh command exits successfully, retry the exact failed command automatically
Do not tell the user to open a separate terminal when the current agent session can run a PTY command.
If gh is installed but not authenticated, explicitly tell the user that GHCR push requires GitHub CLI login, then trigger:
gh auth loginAfter successful login, retry GHCR authentication and continue.
If the user chooses Docker Hub:
docker info 2>/dev/null | grep "Username:"If a Docker Hub session exists, use it:
DOCKER_HUB_USER=<extracted username>
REGISTRY=dockerhubTreat this path as public image only. Do not add Docker Hub private-image credential prompts or Docker Hub pull-secret automation in sealos-deploy.
If no Docker Hub session exists, tell the user:
Docker Hub push requires a local Docker Hub login session.
Please run `docker login` in another terminal, then continue this deploy.4.1 Build & Push
Tag format: <owner-or-user>/<repo-name>:YYYYMMDD-HHMMSS (e.g., ghcr.io/zhujingyang/kite:20260304-143022). The timestamp ensures same-day rebuilds never collide.
Before invoking the build helper, create the build artifact directory:
mkdir -p "$WORK_DIR/.sealos/build"If Node.js available:
node "<SKILL_DIR>/scripts/build-push.mjs" "$WORK_DIR" "<repo-name>" --registry ghcr
node "<SKILL_DIR>/scripts/build-push.mjs" "$WORK_DIR" "<repo-name>" --registry dockerhub --user "<user>"Run the command that matches the user's chosen destination:
- GHCR:
node "<SKILL_DIR>/scripts/build-push.mjs" "$WORK_DIR" "<repo-name>" --registry ghcr - Docker Hub:
node "<SKILL_DIR>/scripts/build-push.mjs" "$WORK_DIR" "<repo-name>" --registry dockerhub
Output: { "success": true, "image": "...", "registry": "ghcr" } or { "success": false, "error": "..." }
For GHCR success, record whether the image is anonymously pullable. If Phase 4 built a GHCR image and it is still private, continue with the GHCR image and let Phase 6 create/update the pull Secret automatically from gh auth token. If Phase 2 reused an existing public image, do not trigger the GHCR pull-secret flow.
If Node.js not available (fallback — run docker directly):
TAG=$(date +%Y%m%d-%H%M%S)If the user chose GHCR:
GH_USER=$(gh api user -q .login)
gh auth token | docker login ghcr.io -u "$GH_USER" --password-stdin
IMAGE="ghcr.io/$GH_USER/<repo-name>:$TAG"
docker buildx build --platform linux/amd64 -t "$IMAGE" --push -f Dockerfile "$WORK_DIR"If the user chose Docker Hub:
DOCKER_HUB_USER=$(docker info 2>/dev/null | sed -n 's/^ Username: //p')
IMAGE="$DOCKER_HUB_USER/<repo-name>:$TAG"
docker buildx build --platform linux/amd64 -t "$IMAGE" --push -f Dockerfile "$WORK_DIR"If $IMAGE is a GHCR image, immediately verify it is anonymously pullable before proceeding:
TOKEN=$(curl -fsSL "https://ghcr.io/token?scope=repository:$GH_USER/<repo-name>:pull" | sed -n 's/.*"token":"\\([^"]*\\)".*/\\1/p')
curl -fsSLI \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json" \
"https://ghcr.io/v2/$GH_USER/<repo-name>/manifests/$TAG"If that check returns 401/403 or the package visibility is still private, continue with the build but mark that Phase 6 must create/update the namespace image pull Secret before rollout. If the run is using an existing public image instead of a new local build, skip this secret-creation path.
4.2 Error Handling
If build fails: 1. Read the error output 2. Load error patterns from internal skill:
<SKILL_DIR>/../dockerfile-skill/knowledge/error-patterns.md3. Match the error → apply fix to Dockerfile → retry 4. Also consult if needed:
<SKILL_DIR>/../dockerfile-skill/knowledge/system-deps.md
<SKILL_DIR>/../dockerfile-skill/knowledge/best-practices.md5. Max 3 retry attempts 6. If still failing → inform user with the specific error and suggest manual review
4.3 Record Result
Always write .sealos/build/build-result.json when Phase 4 runs:
- Success:
outcome: "success"plus pushed image metadata - Failure:
outcome: "failed"plus the captured error message
This avoids leaving an empty build/ directory after a failed build and makes resume/debug behavior inspectable.
On success, record IMAGE_REF from the build output. The build result file is at .sealos/build/build-result.json.
Update analysis.json
On successful build, update .sealos/analysis.json to set image_ref to the built image reference.
---
Phase 5: Generate Sealos Template
5.1 Load Sealos Rules
Read the internal skill's specifications:
<SKILL_DIR>/../docker-to-sealos/SKILL.md — 7-step workflow + MUST rules
<SKILL_DIR>/../docker-to-sealos/references/sealos-specs.md — Sealos ordering, labels, conventions
<SKILL_DIR>/../docker-to-sealos/references/conversion-mappings.md — field-level Docker→Sealos mappingsIf the project uses databases, also read:
<SKILL_DIR>/../docker-to-sealos/references/database-templates.mdIf the project mentions Frappe, ERPNext, HRMS, or bench, also read:
<SKILL_DIR>/../docker-to-sealos/references/frappe-bench.md5.2 Generate Template
Read .sealos/analysis.json and use image_ref, port, databases, and env_vars as inputs.
Generate the template at .sealos/template/index.yaml (overrides the default template/ path from docker-to-sealos skill).
Public URL detection: After generating the base template, check if the app needs its public URL configured:
1. Search source code for common URL config patterns:
- Env vars:
BASE_URL,SITE_URL,APP_URL,NEXTAUTH_URL,PUBLIC_URL,EXTERNAL_URL - Config files:
getConfig(.*[Uu]rl,homeUrl,baseUrl,siteUrlin config patterns - Docker Compose env vars referencing
localhostor placeholder URLs
2. If public URL is needed via env var:
- Add the appropriate env var to the Deployment with value
https://${{ defaults.app_host }}.${{ SEALOS_CLOUD_DOMAIN }}
3. If public URL is needed via config file (e.g., node-config):
- Create a ConfigMap with the minimal config file
- Add volumeMount and volume to the Deployment
- Follow ConfigMap MUST rules (labels, naming, ordering before Deployment)
Critical MUST rules (always apply):
metadata.name: hardcoded lowercase, no variables- Image tag: exact version, never `:latest`
- PVC requests:
<= 1Gi - Container defaults:
cpu: 200m/20m,memory: 256Mi/25Mi - Init containers must define explicit resources; do not rely on namespace defaults. For expensive init work such as framework install, database migration, asset compilation, or
bench new-site, allocate enough memory for the task. imagePullPolicy: IfNotPresentrevisionHistoryLimit: 1automountServiceAccountToken: falsetemplate.spec.imagePullSecrets: [{ name: ${{ defaults.app_name }} }]for managed workloads- App CRD (last resource): only
spec.data.url,spec.displayType,spec.icon,spec.name,spec.type— no other fields (nomenuData,nameColor,template, etc.) - App CRD fixed enums:
spec.displayTypemust benormal;spec.typemust belink
5.3 Validate
Run validation if Python is available:
python "<SKILL_DIR>/../docker-to-sealos/scripts/quality_gate.py" 2>/dev/nullIf Python is not available, validate manually by checking the MUST rules above against the generated YAML.
Template is written to .sealos/template/index.yaml. No separate checkpoint file — the template file's existence is sufficient for resume detection.
---
Phase 5.5: Interactive Configuration
After generating the template, guide the user through application configuration before deployment. This is a critical step — most applications need user-specific configuration to function properly.
5.5.1 Extract Configuration from Template
Parse the generated template YAML and categorize all environment variables and inputs:
Category A — Auto-managed (no user action needed):
defaults.*values:app_name,app_host, random passwords/keys (${{ random(N) }})- Database connections via
secretKeyRef: host, port, username, password from Kubeblocks secrets - Object storage credentials via
secretKeyRef - Composed URLs that reference auto-managed vars (e.g.,
DATABASE_URLbuilt from$(DB_HOST):$(DB_PORT)) - Internal service FQDNs (
*.${{ SEALOS_NAMESPACE }}.svc.cluster.local)
Category B — User-required inputs:
- Template
inputswithrequired: trueand no sensible default - Template
inputswithrequired: trueanddefault: ''; the empty default means the deployer must provide the value before deploy - Env vars with empty or placeholder values that the app cannot function without
- Common examples: admin email, external API keys (OpenAI, SMTP credentials, OAuth client ID/secret)
Category C — Optional with defaults:
- Template
inputswithrequired: falseand reasonable defaults - Env vars user might want to customize but app works without changes
- Common examples: log level, feature toggles, upload size limits, signup enabled/disabled
Category D — Fixed values (informational):
- Hardcoded env vars like
NODE_ENV=production - Port numbers, internal paths
5.5.2 Present Configuration Summary
Display a structured summary to the user. Example:
Configuration for <app-name>:
Auto-configured (no action needed):
- APP_NAME: unique generated name
- DB credentials: from PostgreSQL service (auto-provisioned)
- SECRET_KEY: auto-generated 32-char random string
- REDIS_URL: auto-composed from service credentials
Requires your input:
1. ADMIN_EMAIL — Administrator email address (required)
2. OPENAI_API_KEY — OpenAI API key for AI features (required)
3. SMTP_HOST — SMTP server for sending emails (required if email needed)
Optional (defaults shown, customize if needed):
- LOG_LEVEL: "info"
- MAX_UPLOAD_SIZE: "10M"
- ENABLE_SIGNUP: "true"5.5.3 Collect User Input
For required inputs: 1. Ask the user for each value 2. If user doesn't have a value, explain what it's used for and how to obtain it
- Example: "OPENAI_API_KEY is needed for AI features. Get one at https://platform.openai.com/api-keys"
3. If user wants to skip a feature-gating input (e.g., SMTP), explain which features will be unavailable and set an empty value
For optional inputs: 1. Show the default values 2. Ask: "Do you want to change any of these? (press Enter to keep defaults)" 3. Only update values the user explicitly wants to change
For unfamiliar env vars: If the AI is unsure what a variable does, read the project README, .env.example, or source code to explain it to the user before asking for a value.
5.5.4 Apply Configuration to Template
Update the template's inputs section with user-provided values:
# Before (generated)
inputs:
ADMIN_EMAIL:
description: 'Administrator email address'
type: string
default: ''
required: true
# After (user configured)
inputs:
ADMIN_EMAIL:
description: 'Administrator email address'
type: string
default: 'admin@example.com'
required: trueWrite the updated template back to .sealos/template/index.yaml.
Record all user choices as CONFIG for use in Phase 6:
CONFIG.args = { ADMIN_EMAIL: "admin@example.com", OPENAI_API_KEY: "sk-..." }These args will be passed to the Template API's args field (Phase 6.2), which overrides or supplies spec.inputs in the template.
5.5.5 Deployment Confirmation
Before proceeding to Phase 6, present a final summary and ask for confirmation:
Ready to deploy <app-name> to Sealos Cloud:
Image: zhujingyang/app:20260309
Region: https://usw-1.sealos.io
Database: PostgreSQL 16 (auto-provisioned)
Config: 3 required inputs configured, 2 optional defaults kept
Proceed with deployment? (y/n)Wait for user confirmation before continuing to Phase 6.
Configuration is applied directly to .sealos/template/index.yaml. No separate checkpoint — the template contains the final configured state.
---
Phase 6: Deploy to Sealos Cloud
6.1 Construct Deploy URL
The template deploy API uses a fixed template. subdomain prefix on the region domain:
Region example: https://usw-1.sealos.io
Deploy URL example: https://template.usw-1.sealos.io/api/v2alpha/templates/rawDo not send requests to the literal placeholder form https://template.<region-domain>/.... Always derive REGION_DOMAIN first, then build DEPLOY_URL from the real value.
Extract the region from ~/.sealos/auth.json (saved during preflight auth):
REGION=$(jq -r '.region' ~/.sealos/auth.json)
REGION_DOMAIN=$(printf '%s' "$REGION" | sed -E 's#^https?://##; s#/$##')
DEPLOY_URL="https://template.${REGION_DOMAIN}/api/v2alpha/templates/raw"6.2 Deploy Template
Read kubeconfig, encode it with `encodeURIComponent`, and send as Authorization header.
Request body fields:
yaml(required) — the full template YAML stringargs(optional) — template variable key-value pairs that override or supplyspec.inputsfields. Values from Phase 5.5CONFIG.args.dryRun(optional, boolean) — if true, validates resources against K8s API without creating anything. Returns 200 with preview.
With Node.js (preferred):
node "<SKILL_DIR>/scripts/deploy-template.mjs" ".sealos/template/index.yaml" --dry-run
node "<SKILL_DIR>/scripts/deploy-template.mjs" ".sealos/template/index.yaml" --args-json '{"ADMIN_EMAIL":"user@example.com"}'This script is the preferred execution path because it:
- reads
~/.sealos/auth.jsondirectly instead of fragile shell parsing - derives
REGION_DOMAINfrom the realregionvalue - always posts to the concrete
DEPLOY_URL - emits structured JSON on success or failure
Without Node.js (curl fallback):
# encodeURIComponent via Python (almost always available)
KUBECONFIG_ENCODED=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.stdin.read(), safe=''))" < ~/.sealos/kubeconfig)
# Build JSON body with args — use jq if available
TEMPLATE_YAML=$(cat .sealos/template/index.yaml)
jq -n --arg yaml "$TEMPLATE_YAML" \
--argjson args '{"ADMIN_EMAIL":"user@example.com"}' \
'{yaml: $yaml, args: $args}' | \
curl -sf -X POST "$DEPLOY_URL" \
-H "Authorization: $KUBECONFIG_ENCODED" \
-H "Content-Type: application/json" \
-d @-Without jq: The AI should read the template YAML (already in context), construct the JSON body directly, write it to a temp file, and curl it:
# AI writes properly escaped JSON to temp file including args from Phase 5.5
cat > /tmp/sealos-deploy-body.json << 'DEPLOY_EOF'
{"yaml": "<AI inserts JSON-escaped template YAML here>", "args": {"ADMIN_EMAIL": "user@example.com"}}
DEPLOY_EOF
curl -sf -X POST "$DEPLOY_URL" \
-H "Authorization: $KUBECONFIG_ENCODED" \
-H "Content-Type: application/json" \
-d @/tmp/sealos-deploy-body.json
rm -f /tmp/sealos-deploy-body.json6.3 Handle Response
All error responses use a unified format:
{ "error": { "type": "...", "code": "...", "message": "...", "details": ... } }| Status | Meaning | Action |
|---|---|---|
| 201 | Deployed successfully | Extract instance name and resources from response |
| 200 | Dry-run preview (dryRun: true) | Show resource preview and quota |
| 400 | Validation error — INVALID_PARAMETER (missing yaml/name) or INVALID_VALUE (bad YAML, missing required args) | Read error.message, fix template or provide missing args, retry |
| 401 | AUTHENTICATION_REQUIRED — missing or invalid kubeconfig | Re-run auth: node sealos-auth.mjs login, or switch workspace: node sealos-auth.mjs switch <ns> |
| 403 | FORBIDDEN — insufficient permissions | Inform user, check kubeconfig namespace permissions |
| 409 | ALREADY_EXISTS — instance already exists | Inform user, suggest different app name |
| 422 | RESOURCE_ERROR — K8s rejected resource spec | Read error.details for K8s rejection reason, fix template |
| 503 | SERVICE_UNAVAILABLE — K8s cluster unreachable | Fall back to kubectl (6.4) |
On 201 success, the response contains:
{
"name": "myapp-abcdefgh",
"uid": "...",
"resourceType": "instance",
"displayName": "...",
"createdAt": "...",
"args": { ... },
"resources": [
{ "name": "myapp-abcdefgh", "uid": "...", "resourceType": "deployment", "quota": { "cpu": 0.1, "memory": 0.25, "storage": 0, "replicas": 1 } }
]
}Extract the instance name and present to user.
6.3.1 Post-Deploy Readiness Verification
After a 201 response, do not assume the app is usable. Verify Kubernetes readiness:
NAMESPACE=$(KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
config view --minify -o jsonpath='{.contexts[0].context.namespace}')
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get pod,svc,endpoints,ingress -n "$NAMESPACE" -l app=<app-name>For the public app Service, endpoints must be non-empty before the Ingress can serve traffic. If the URL returns no healthy upstream or HTTP 503:
1. Check endpoints/<app-name>; empty endpoints means the backend Pod is not Ready. 2. Check Pod init container status and previous logs:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
logs pod/<pod> -n "$NAMESPACE" -c <init-container> --previous --tail=2003. Look for common signatures:
OOMKilledor exit137: increase init container memory and recreate the Pod.Permission deniedon mounted paths: addfsGroupor a one-shot permission repair for existing PVCs.- App-specific migration/bootstrap errors: repair the failed bootstrap state, then rerun the init path.
4. Only report the app as usable after the endpoint exists and an HTTP request to the public URL returns a non-5xx response. 5. Continue to Phase 6.5 before writing deployment state or reporting success.
For templates with KubeBlocks-supported databases, runtime truth must include the database control plane and generated connection surface:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get cluster,component,instanceset,secret,svc -n "$NAMESPACE" \
| grep -E '<app-name>|redis|postgres|mysql|mongo|broker'Acceptance requires the KubeBlocks Cluster to be Ready/Running, each expected Component and InstanceSet to converge, the account Secret to exist, and the application environment to point at the expected Service FQDN. For Redis, verify both redis and redis-sentinel components, ${APP_NAME}-redis-redis-account-default, and ${APP_NAME}-redis-redis-redis.${NAMESPACE}.svc.cluster.local. For MongoDB, verify ${APP_NAME}-mongo-mongodb-account-root or the matching mongodb suffix variant before judging app initialization.
6.4 Fallback: kubectl apply (when Template API is unavailable)
If the Template API returns 503/500 or is unreachable, deploy directly via kubectl using the local kubeconfig.
Step 1 — Gather cluster context:
# User namespace
NAMESPACE=$(KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify config view --minify -o jsonpath='{.contexts[0].context.namespace}')
# Cluster domain (from region URL)
CLOUD_DOMAIN=$(jq -r '.region' ~/.sealos/auth.json | sed -E 's#^https?://##; s#/$##')
# TLS secret name (from existing ingress, or default)
CERT_SECRET=$(KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify get ingress -n "$NAMESPACE" -o jsonpath='{.items[0].spec.tls[0].secretName}' 2>/dev/null || echo "wildcard-cert")Step 2 — Render template variables:
The template YAML from Phase 5 contains ${{ }} variables. The AI must replace them with actual values:
| Variable | Value |
|---|---|
${{ defaults.app_name }} | Generate: <app>-<random8> (e.g., edict-xn22k4ie) |
${{ defaults.app_host }} | Generate: <app>-<random8> (e.g., edict-2v4jryz1) |
${{ defaults.<key> }} | Other defaults: generate per their value pattern |
${{ inputs.<key> }} | User-provided values from Phase 5.5 CONFIG.args |
${{ random(N) }} | Random alphanumeric string of length N |
${{ SEALOS_CLOUD_DOMAIN }} | CLOUD_DOMAIN from Step 1 |
${{ SEALOS_CERT_SECRET_NAME }} | CERT_SECRET from Step 1 |
${{ SEALOS_NAMESPACE }} | NAMESPACE from Step 1 |
Important: ${{ inputs.xxx }} values come from the user in Phase 5.5. If any required input was not provided, the AI must ask the user now before proceeding.
The AI reads the template YAML, performs all variable substitutions, and produces rendered K8s resource documents.
Step 3 — Split and apply:
The rendered YAML is a multi-document file (separated by ---). Split it into individual resources:
1. Skip the first document (kind: Template) — this is the Sealos template metadata, not a K8s resource 2. Apply the remaining documents (Deployment, Service, Ingress, App, etc.) via kubectl:
# AI writes the rendered resources (without the Template CR) to a temp file
cat > /tmp/sealos-deploy-rendered.yaml << 'EOF'
<rendered Deployment + Service + Ingress + App YAML>
EOF
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify apply -f /tmp/sealos-deploy-rendered.yaml -n "$NAMESPACE"
rm -f /tmp/sealos-deploy-rendered.yamlStep 4 — Handle apply errors:
| Error | Fix |
|---|---|
unknown field "spec.xxx" in App CR | Remove the unknown field and retry |
| PodSecurity warnings | Warnings are non-blocking — deployment still proceeds |
Forbidden | Kubeconfig may be expired — re-run auth |
already exists | Resource exists from a previous deploy — use kubectl apply (idempotent) |
Step 5 — Verify deployment:
# Wait for pod to be ready (max 120s)
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
wait --for=condition=available deployment/<app-name> -n "$NAMESPACE" --timeout=120s
# Get pod status
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get pods -l app=<app-name> -n "$NAMESPACE"App URL: https://<app_host>.<CLOUD_DOMAIN>
6.5 Runtime Truth Pass
Run this pass after Template API deploy or kubectl fallback deploy. The deployment is accepted only after the live application entry, logs, and first meaningful user workflow are verified.
Read the app URL from the live App resource when possible:
APP_URL=$(KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get apps.app.sealos.io/<app-name> -n "$NAMESPACE" \
-o jsonpath='{.spec.data.url}' 2>/dev/null)If the live App resource has no URL, use the URL returned by Template API or the rendered fallback URL.
Collect the runtime footprint:
node "<SKILL_DIR>/scripts/sealos-footprint.mjs" --namespace "$NAMESPACE" --app "<app-name>"For every web application:
node "<SKILL_DIR>/scripts/sealos-live-smoke.mjs" --url "$APP_URL"For login-gated web applications, identify the first-run, registration, or login flow from upstream docs, source code, the rendered template, or observed network/API behavior. Complete the flow and verify at least one authenticated page or API route. If administrator credentials were collected in Phase 5.5, use those exact deploy-time values for the login smoke. Mask the password in command echoes, logs, summaries, and final output.
When credentials and API paths are known, use the helper for the repeatable HTTP portion:
node "<SKILL_DIR>/scripts/sealos-live-smoke.mjs" \
--url "$APP_URL" \
--captcha-path "/api/get_validate_code" \
--login-path "/api/login" \
--username "$ADMIN_USER" \
--password "$ADMIN_PASSWORD" \
--auth-path "/api/languages/get"After the browser/API smoke, inspect recent logs again:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
logs -n "$NAMESPACE" pod/<pod> --all-containers --tail=300Inspect the live main container startup command for managed app workloads:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
get pod/<pod> -n "$NAMESPACE" \
-o jsonpath='{range .spec.containers[*]}{.name}{" command="}{.command}{" args="}{.args}{"\n"}{end}'Acceptance checklist:
- Pods and initContainers are complete or ready.
- Service endpoints are populated.
- The actual App URL loads from a fresh session.
- Login-gated apps complete setup/login with deploy-time administrator credentials and one authenticated action. Passwords remain masked in all output.
- SSR/browser failure text such as
Application error,server-side exception,Internal Server Error, andUnhandled Runtime Erroris absent from smoke responses. - Recent logs are clear of recurring startup, migration, bootstrap, and access-control failures.
- Main business containers keep
command/argsshort and close to the official entrypoint; repeated file preparation, permission repair, database bootstrap, or compatibility self-healing belongs in initContainers, Jobs, or ConfigMap scripts. - Shell wrappers in main containers end with
exec <final-process>so signal handling remains correct. - Database-backed apps have the expected live database objects, because Job completion or TTL cleanup is only historical evidence.
For app-specific guidance, load:
<SKILL_DIR>/references/live-smoke-playbooks.mdWrite state.json
This is critical for enabling future updates. After a successful deploy, write .sealos/state.json:
{
"version": "1.0",
"last_deploy": {
"app_name": "<instance name, e.g. evershop-uvbp0n0n>",
"app_host": "<ingress host prefix, e.g. evershop-4ha6b4mh>",
"namespace": "<K8s namespace from kubeconfig>",
"region": "<Sealos region domain, e.g. gzg.sealos.run>",
"image": "<IMAGE_REF used in this deploy>",
"docker_hub_user": "<DOCKER_HUB_USER, or null if existing image was used>",
"repo_name": "<REPO_NAME>",
"url": "<public app URL>",
"deployed_at": "<current ISO timestamp>",
"last_updated_at": "<current ISO timestamp>"
},
"history": [
{
"at": "<current ISO timestamp>",
"action": "deploy",
"image": "<IMAGE_REF>",
"method": "<template-api or kubectl-apply>",
"status": "success",
"note": "Initial deployment"
}
]
}The last_deploy section is what Deployment Mode Detection reads on subsequent runs to decide between DEPLOY and UPDATE mode. Without it, every /sealos-deploy creates a new instance.
The history array is append-only — every subsequent update (via Update Path) adds an entry. See the Update History section at the end of this file for the full schema and rules.
Sources for each field:
app_name: from Template API responsenameor the rendereddefaults.app_name(kubectl apply)app_host: from the rendereddefaults.app_hostvalue, or parsed from the Ingress hostnamespace: from kubeconfig contextregion: from~/.sealos/auth.jsonregionfield (striphttps://)image: fromanalysis.jsonimage_refdocker_hub_user: from Phase 4DOCKER_HUB_USER(null if Phase 2 found existing image)repo_name: fromanalysis.jsonproject.repo_nameurl: constructed fromapp_hostandregion
---
Cleanup
If WORK_DIR was created via mktemp (remote GitHub URL clone), remove it:
rm -rf "$WORK_DIR"Do NOT clean up if WORK_DIR is the user's local project directory.
For test deployments, delete the Sealos Instance and application resources before database RBAC. Keep KubeBlocks ServiceAccount, Role, and RoleBinding resources until the database Cluster finalizer has converged. When a Cluster or Component remains in Deleting after dependent pods and InstanceSets are gone, inspect the finalizers and use finalizer removal only as the last recovery step after recording the stuck resource and owner references.
---
Output
On success, present to user:
✓ Assessed: {language} + {framework}, score {N}/12 — {verdict}
✓ Image: {IMAGE_REF} ({source: existing/built})
✓ Template: .sealos/template/index.yaml
✓ Configured: {N} inputs set ({M} required, {K} optional)
✓ Deployed to Sealos Cloud ({region})
App URL: https://<app-access-url>
To update this deployment later, run: /sealos-deployIf any inputs were configured, also show:
Configuration applied:
ADMIN_EMAIL: admin@example.com
OPENAI_API_KEY: sk-***...*** (masked)Mask sensitive values (API keys, passwords) — show only first 3 and last 3 characters.
--- ---
Update Path
This section is only executed in UPDATE mode (entered via Deployment Mode Detection above).
The update path skips Assess, Detect Image, Dockerfile, and Template generation — it reuses the existing deployment and only pushes a new image.
All kubectl commands use the Sealos kubeconfig:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verifyReminder: kubectl delete requires user confirmation — see SKILL.md "kubectl Safety Rules".
Context from Mode Detection
These values are already known from .sealos/state.json last_deploy section:
APP_NAME = last_deploy.app_name (e.g., "evershop-uvbp0n0n")
NAMESPACE = last_deploy.namespace (e.g., "ns-qiqovyrm")
REGION = last_deploy.region (e.g., "gzg.sealos.run")
CURRENT_IMAGE = last_deploy.image (e.g., "zhujingyang/evershop:20260309")
DOCKER_HUB_USER = last_deploy.docker_hub_user
REPO_NAME = last_deploy.repo_name
APP_URL = last_deploy.url---
Phase U1: Build & Push
Ask the user what changed:
What would you like to update?
1. Code changed — rebuild and push new image (default)
2. Just restart the current deployment (no rebuild)Option 1: Rebuild
Reuse the exact same build logic as Phase 4 — same Dockerfile, same explicit registry choice, same build-push.mjs or fallback. Default to the registry used by CURRENT_IMAGE, but let the user switch if they want.
# With Node.js:
node "<SKILL_DIR>/scripts/build-push.mjs" "$WORK_DIR" "$REPO_NAME" --registry ghcr
node "<SKILL_DIR>/scripts/build-push.mjs" "$WORK_DIR" "$REPO_NAME" --registry dockerhub
# Without Node.js:
TAG=$(date +%Y%m%d-%H%M%S)
NEW_IMAGE="<selected-user>/$REPO_NAME:$TAG"
docker buildx build --platform linux/amd64 -t "$NEW_IMAGE" --push -f Dockerfile "$WORK_DIR"Record NEW_IMAGE from the output.
If build fails → same error handling as Phase 4.2 (read error-patterns.md, fix Dockerfile, retry up to 3 times).
Option 2: Restart only
No build needed. Use the current image:
NEW_IMAGE = CURRENT_IMAGEWill trigger a rollout restart in Phase U2.
---
Phase U2: Apply Update
Image update (Option 1 — new image built):
If NEW_IMAGE starts with ghcr.io/, create or refresh the app-scoped pull Secret and make sure the existing Deployment references it before swapping images:
node "<SKILL_DIR>/scripts/ensure-image-pull-secret.mjs" "$NAMESPACE" "$APP_NAME" "$NEW_IMAGE" "$APP_NAME"KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
set image deployment/$APP_NAME \
$APP_NAME=$NEW_IMAGE \
-n $NAMESPACERestart only (Option 2 — no new image):
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
rollout restart deployment/$APP_NAME \
-n $NAMESPACE---
Phase U3: Verify Rollout
Wait for new pods to be ready:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
rollout status deployment/$APP_NAME \
-n $NAMESPACE --timeout=120sOn success:
Update .sealos/state.json:
- Set
last_deploy.imagetoNEW_IMAGE - Set
last_deploy.last_updated_atto current ISO timestamp - Append an entry to
history(see Update History below)
Present to user:
✓ Updated: <APP_NAME>
✓ Image: <CURRENT_IMAGE> → <NEW_IMAGE>
✓ Rollout: complete
App URL: <APP_URL>
To update again later, run: /sealos-deployOn failure:
Auto-rollback:
KUBECONFIG=~/.sealos/kubeconfig kubectl --insecure-skip-tls-verify \
rollout undo deployment/$APP_NAME \
-n $NAMESPACEAppend a failed entry to history in .sealos/state.json (see Update History below).
Report to user:
✗ Rollout failed — automatically rolled back to previous version.
Debug:
kubectl logs deployment/<APP_NAME> -n <NAMESPACE> --tail=50Do NOT update last_deploy.image on failure — it stays at the old value.
---
Update History
Every update (successful or failed) appends an entry to history in .sealos/state.json. This provides a traceable log of all changes to the deployment.
{
"version": "1.0",
"last_deploy": {
"app_name": "morphic-dc21ad72",
"image": "zhujingyang/morphic:20260310-143022"
},
"history": [
{
"at": "2026-03-09T18:37:30Z",
"action": "deploy",
"image": "ghcr.io/miurla/morphic:668daf0e",
"method": "kubectl-apply",
"status": "success",
"note": "Initial deployment"
},
{
"at": "2026-03-09T20:15:00Z",
"action": "set-env",
"changes": ["OPENAI_API_KEY=sk-***", "OPENAI_BASE_URL=https://..."],
"method": "kubectl-set-env",
"status": "success",
"note": "Fix: default openai provider not enabled"
},
{
"at": "2026-03-10T14:30:22Z",
"action": "set-image",
"previous_image": "ghcr.io/miurla/morphic:668daf0e",
"image": "zhujingyang/morphic:20260310-143022",
"method": "kubectl-set-image",
"status": "success"
},
{
"at": "2026-03-11T09:00:00Z",
"action": "set-image",
"previous_image": "zhujingyang/morphic:20260310-143022",
"image": "zhujingyang/morphic:20260311-090000",
"method": "kubectl-set-image",
"status": "failed",
"note": "CrashLoopBackOff — rolled back"
}
]
}History entry fields
| Field | Required | Description |
|---|---|---|
at | yes | ISO 8601 timestamp of the operation |
action | yes | What changed: deploy, set-image, set-env, patch, restart |
status | yes | success or failed |
method | yes | kubectl command used: kubectl-apply, kubectl-set-image, kubectl-set-env, kubectl-patch, kubectl-rollout-restart |
image | if image changed | New image reference |
previous_image | if image changed | Image before the update |
changes | if env/config changed | Array of changes (mask sensitive values: sk-***) |
note | no | Free-text reason or context for the change |
Rules
- Always append, never rewrite — history is append-only. Never delete or modify previous entries.
- Mask secrets — API keys, passwords, tokens: show only first 3 chars +
***(e.g.,sk-***). - Initial deploy counts — the first entry should be
action: "deploy"written by Phase 6 checkpoint. - Failed updates count — record failures so the user can see what was attempted and why it didn't work.
- Keep it bounded — if history exceeds 50 entries, trim the oldest entries (keep the first
deployentry and the most recent 49).
6.1.5 Ensure Image Pull Secret (locally built private GHCR path only)
Before calling the Template API or kubectl apply, check whether this run actually passed through Phase 4 local build and push. This step is only for cases where:
- Phase 4 built a new GHCR image locally with Docker
- That GHCR image is not anonymously pullable
Do not run this step when:
- Phase 2 reused an existing public image
- The selected registry was Docker Hub public image flow
The template itself should reference the app-scoped pull Secret name via:
imagePullSecrets:
- name: ${{ defaults.app_name }}If the run meets the locally built private-GHCR conditions above, create or update the app-scoped pull Secret in the target namespace using the local gh CLI session:
node "<SKILL_DIR>/scripts/ensure-image-pull-secret.mjs" "$NAMESPACE" "$APP_NAME" "$IMAGE_REF"Behavior:
- Uses
gh api user -q .loginandgh auth token - Creates/updates a
docker-registrySecret named exactly like the app ($APP_NAME) - When a deployment name is provided, also patches
spec.template.spec.imagePullSecretsto include that app-scoped Secret - Keeps registry credentials out of the generated template inputs
- Do not call it for existing public images
This step should run for both fresh deploys and in-place updates before rollout, but only on the locally built private-GHCR path.
Phase 0: Preflight
Detect the user's environment, record what's available, guide them to fix what's missing.
Hard rule: Every run must start with a preflight capability scan before touching the project. That means:
- Detect tool availability first
- Detect auth/workspace state first
- Record which later phases are currently blocked
Preflight is responsible for early detection, but only some failures are immediate stop conditions. Do not treat Docker, gh, or buildx as universal entry requirements — they become mandatory only if the run actually needs local image build/push.
Tool Install Policy
When docker, gh, or kubectl is missing, do not just print commands and stop. Ask directly:
Missing <tool>. Install it now? (y/n)If the user answers y, install the tool for the current platform, then re-run the corresponding check. If the install command needs elevated privileges, package-manager setup, or manual UI interaction, explain that before running it.
Step 1: Environment Detection
Detect the local toolchain on every run. These checks are fast, and re-running them avoids stale results after the user installs a missing dependency such as gh or kubectl.
1.1 Detect Installed Tools
Run all checks:
# Commonly needed
docker --version 2>/dev/null
git --version 2>/dev/null
# Optional (enables script acceleration)
node --version 2>/dev/null
python3 --version 2>/dev/null
# Optional (enables GHCR push — preferred over Docker Hub)
gh --version 2>/dev/null
# Conditional (required for update-mode rollout operations)
# Check PATH first, then fallback to ~/.agents/bin/
kubectl version --client 2>/dev/null || ~/.agents/bin/kubectl version --client 2>/dev/null
# Always available (system built-in)
curl --version 2>/dev/null | head -1
which jq 2>/dev/nullVersion strings are present when installed, null when missing.
Record as ENV:
ENV.docker = true/false
ENV.git = true/false
ENV.node = true/false
ENV.python = true/false
ENV.kubectl = true/false (required for update-mode rollout operations)
ENV.gh = true/false (enables zero-interaction GHCR push)
ENV.curl = true/false
ENV.jq = true/false1.2 Docker Daemon Check
Tool detection and Docker daemon status are different checks. Always verify the daemon separately:
docker info 2>/dev/null- Not installed → guide by platform:
- Ask:
Missing Docker. Install it now? (y/n) - If user answers
y: - macOS: run
brew install --cask docker, then tell the user to open Docker Desktop - Linux: run
curl -fsSL https://get.docker.com | sh - Installed but daemon not running → "Please start Docker Desktop (macOS) or
sudo systemctl start docker(Linux)."
git — if missing:
brew install git(macOS) orsudo apt install git(Linux)
Optional and Path-Dependent Tools
gh CLI (GitHub CLI):
- If present and authenticated → enables zero-interaction GHCR push
build-push.mjsauto-detectsgh auth statusand usesgh auth tokento login toghcr.io- GHCR push alone is not enough for Sealos. For private GHCR packages, the deploy step must create an image pull Secret using the local
ghCLI session. sealos-deployshould never ask the user to type registry host/username/password whengh auth statusis already available locally.- Missing
ghis not a universal preflight failure ghbecomes mandatory only when the selected image destination is GHCR- If
ghis missing, ask: Missing gh. Install it now? (y/n)- If user answers
y: - macOS: run
brew install gh - Debian/Ubuntu: run
sudo apt install gh - Do not trigger
gh auth loginduring environment detection - Only trigger
gh auth loginlater if the run actually reaches a GHCR push path chosen by the user
Docker Hub login session:
- Needed only when the selected image destination is Docker Hub
- Docker Hub path assumes the pushed image will be public at deploy time
- Private Docker Hub images are out of scope for
sealos-deploypull-secret automation docker loginmay need to be run manually by the user in another terminal- Do not treat a missing Docker Hub login as a universal preflight blocker
- Ask for the registry destination later in Phase 4, then enforce the matching login path
Node.js:
- If missing, no problem. Pipeline uses fallback mode:
score-model.mjs→ AI reads files and applies scoring rules directlydetect-image.mjs→ AI runs curl commands for Docker Hub / GHCR APIbuild-push.mjs→ AI runsdocker buildxcommands directlysealos-auth.mjs→ AI runs curl to exchange token for kubeconfig (workspace list/switch not available in fallback mode)
Python:
- If missing, Sealos template validation (Phase 5) uses AI self-check instead of
quality_gate.py
kubectl (required for in-place updates):
- Needed for updating already-deployed apps with
kubectl set imageandkubectl rollout - If
kubectlis missing, ask: Missing kubectl. Install it now? (y/n)- If user answers
y: - macOS: run
brew install kubectl - Debian/Ubuntu: run
sudo apt install kubectl - If
kubectlis available outside PATH, use the absolute path for all kubectl commands
Step 2: Capability Classification
Before touching the project, classify findings into:
- immediate stop conditions
- warnings that may become blocking later
- optional accelerators
2.1 Immediate Stop Conditions
Stop before project inspection only when one of these is true:
- Sealos authentication is unavailable and cannot be completed
- Workspace selection is incomplete
- The user provided a GitHub URL and
gitis unavailable, so the repository cannot be cloned curlis unavailable, so auth and fallback API checks cannot run
These are true entry blockers for a deploy run.
2.2 Build-Path Warnings
Detect these now and report them early, but do not stop the run yet:
- Docker CLI missing
- Docker daemon not running
ghmissinggh auth statusfailing- Docker builder unavailable (
docker buildx versionor equivalent) - Container registry connectivity looks unhealthy
These findings become hard blockers only if the run later determines that local image build/push is required.
2.3 Update-Path Warnings
Detect these now and report them early, but do not stop a fresh deploy:
kubectlmissing- kubeconfig present but unusable
These become hard blockers only if the run enters UPDATE mode or needs rollout verification through kubectl.
2.4 Early Reporting Rule
At the end of preflight, explicitly tell the user:
- which items are ready
- which items are warnings only
- which later path each warning would block
Example:
- "Docker is not ready. This will block Phase 4 local build, but we can still continue to detect whether an existing image can be reused."
- "
kubectlis missing. Fresh deploy can continue, but UPDATE mode and rollout verification will be blocked until it is installed."
Step 3: Project Context
Execution order override: Do not execute this section until Step 4 auth/workspace checks are complete. Run Step 4: Sealos Cloud Auth first, satisfy the immediate stop conditions, then come back to Step 3.
This section is intentionally documented here for readability, but it is operationally blocked behind Step 4.
Determine what we're deploying and gather project information.
2.1 Resolve Working Directory
A) User provided a GitHub URL:
WORK_DIR=$(mktemp -d)
git clone --depth 1 "<github-url>" "$WORK_DIR"
GITHUB_URL="<github-url>"B) User provided a local path:
WORK_DIR="<local-path>"C) No input — deploy current project (most common):
WORK_DIR="$(pwd)"2.2 Git Repo Detection
# Is it a git repo?
git -C "$WORK_DIR" rev-parse --is-inside-work-tree 2>/dev/null
# Git metadata
git -C "$WORK_DIR" remote get-url origin 2>/dev/null # → GITHUB_URL (if github.com)
git -C "$WORK_DIR" branch --show-current 2>/dev/null # → BRANCH
git -C "$WORK_DIR" log --oneline -1 2>/dev/null # → latest commitRecord:
PROJECT.work_dir = resolved path
PROJECT.is_git = true/false
PROJECT.github_url = "https://github.com/owner/repo" or empty
PROJECT.repo_name = basename of directory or parsed from URL
PROJECT.branch = current branchIf PROJECT.github_url exists, parse owner and repo for Phase 2 image detection.
2.3 Read README
README is the single most important file for understanding a project. Read it now.
# Find README (case-insensitive)
ls "$WORK_DIR"/README* "$WORK_DIR"/readme* 2>/dev/null | head -1Read the README content and extract:
- Project description — what does this project do?
- Tech stack — language, framework, database
- Run/build instructions — how to build, what port it listens on
- Docker references —
docker run,docker pull, image names (ghcr.io/..., dockerhub/...) - Environment variables — any
.envexamples or config descriptions
Record key findings in PROJECT.readme_summary for use in Phase 1 (assess) and Phase 2 (detect).
This avoids re-reading README in every phase. The AI already has it in context.
Step 4: Sealos Cloud Auth (OAuth2 Device Grant Flow)
This step must complete before Step 3 project context begins in practice.
Uses RFC 8628 Device Authorization Grant — no token copy-paste needed.
4.0 Region Selection
Before auth, let the user choose which Sealos Cloud region to deploy to.
Read the default region and available regions from config:
DEFAULT_REGION=$(jq -r '.default_region' "<SKILL_DIR>/config.json")Always ask the user to confirm or choose a region. Present the regions from config.json and allow custom input:
Which Sealos Cloud region do you want to deploy to?
1. https://usw-1.sealos.io (default)
2. https://gzg.sealos.run
3. https://bja.sealos.run
4. https://hzh.sealos.run
5. Enter a custom region URL
Default: https://usw-1.sealos.ioThe region list comes from config.json regions array. If regions is not present, show only default_region.
If the user has an existing ~/.sealos/auth.json, read the previously used region and offer it as an option:
PREV_REGION=$(jq -r '.region // empty' ~/.sealos/auth.json 2>/dev/null)If PREV_REGION exists and differs from DEFAULT_REGION, include it in the choices.
Record the user's choice as REGION for use throughout the rest of this step and Phase 6.
If the user picks a different region than the existing `~/.sealos/auth.json`, the existing kubeconfig is invalid — force re-authentication.
4.1 Check auth status:
With Node.js:
node "<SKILL_DIR>/scripts/sealos-auth.mjs" checkReturns: { "authenticated": true/false, "kubeconfig_path": "...", "workspace": "ns-xxx" }
Without Node.js:
test -f ~/.sealos/kubeconfig && echo '{"authenticated":true}' || echo '{"authenticated":false}'4.2 If not authenticated — Device Grant Login:
With Node.js (recommended):
node "<SKILL_DIR>/scripts/sealos-auth.mjs" login [region-url]If the script fails with "error":"fetch failed" or TLS/certificate error, retry with --insecure:
node "<SKILL_DIR>/scripts/sealos-auth.mjs" login [region-url] --insecureIf it still fails, fall back to curl (see below). Once you switch to curl, use curl for the entire remaining flow — do NOT mix curl and Node.js mid-flow.
The script will: 1. POST <region>/api/auth/oauth2/device with the client_id from config.json 2. Output a verification URL and user code to stderr 3. Auto-open the browser for the user 4. Poll POST <region>/api/auth/oauth2/token every 5s until approved 5. Exchange access_token for regional token via POST <region>/api/auth/regionToken 6. Save kubeconfig to ~/.sealos/kubeconfig (mode 0600) 7. Save access_token, regional_token, and current_workspace to ~/.sealos/auth.json
Important — AI must always show the clickable URL to the user: Even though the script attempts to auto-open the browser, it may fail (e.g., headless environment, SSH session, sandbox restrictions). After running the script, YOU (the AI) must extract the verification URL from stderr output and display it as a clickable link to the user:
Please click the link below to authorize:
<verification_uri_complete>
Authorization code: <user_code>This ensures the user can always complete authorization regardless of whether auto-open succeeded.
Stdout outputs JSON result: { "kubeconfig_path": "...", "region": "...", "workspace": "ns-xxx" }
Without Node.js (curl fallback):
Important: once you enter the curl path, complete ALL steps with curl. Do NOT switch to Node.js or Python mid-flow.
First, read constants from <SKILL_DIR>/config.json:
# Read skill constants (client_id, default_region)
CLIENT_ID=$(jq -r '.client_id' "<SKILL_DIR>/config.json")
DEFAULT_REGION=$(jq -r '.default_region' "<SKILL_DIR>/config.json")Step 1 — Request device authorization:
REGION="${REGION:-$DEFAULT_REGION}"
DEVICE_RESP=$(curl -ksf -X POST "$REGION/api/auth/oauth2/device" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code")Note: -k skips TLS verification for self-signed certificates.
Extract fields from response:
DEVICE_CODE=$(echo "$DEVICE_RESP" | grep -o '"device_code":"[^"]*"' | cut -d'"' -f4)
USER_CODE=$(echo "$DEVICE_RESP" | grep -o '"user_code":"[^"]*"' | cut -d'"' -f4)
VERIFY_URL=$(echo "$DEVICE_RESP" | grep -o '"verification_uri_complete":"[^"]*"' | cut -d'"' -f4)
INTERVAL=$(echo "$DEVICE_RESP" | grep -o '"interval":[0-9]*' | cut -d: -f2)
INTERVAL=${INTERVAL:-5}Step 2 — Show the authorization link to user:
Please click the link below to authorize:
$VERIFY_URL
Authorization code: $USER_CODEIf VERIFY_URL is empty, use verification_uri instead and show the user code separately.
Step 3 — Poll for token:
while true; do
sleep "$INTERVAL"
TOKEN_RESP=$(curl -ksf -X POST "$REGION/api/auth/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=$DEVICE_CODE")
# Check for access_token in response
ACCESS_TOKEN=$(echo "$TOKEN_RESP" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
if [ -n "$ACCESS_TOKEN" ]; then
break
fi
# Check for terminal errors
ERROR=$(echo "$TOKEN_RESP" | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
case "$ERROR" in
authorization_pending) continue ;;
slow_down) INTERVAL=$((INTERVAL + 5)) ;;
access_denied) echo "User denied authorization"; exit 1 ;;
expired_token) echo "Device code expired"; exit 1 ;;
*) echo "Error: $ERROR"; exit 1 ;;
esac
doneStep 4 — Exchange token for regional token + kubeconfig (still curl):
REGION_RESP=$(curl -ksf -X POST "$REGION/api/auth/regionToken" \
-H "Authorization: $ACCESS_TOKEN" \
-H "Content-Type: application/json")
# Server returns { data: { token, kubeconfig } }
REGIONAL_TOKEN=$(echo "$REGION_RESP" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)
# Extract kubeconfig — it's a multi-line YAML value inside JSON
mkdir -p ~/.sealos
node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8')); process.stdout.write(d.data.kubeconfig)" <<< "$REGION_RESP" > ~/.sealos/kubeconfig 2>/dev/null \
|| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['kubeconfig'])" <<< "$REGION_RESP" > ~/.sealos/kubeconfig
chmod 600 ~/.sealos/kubeconfigNote: kubeconfig is multi-line YAML embedded in JSON — simple grep won't work. Use node/python one-liner to extract it. Save auth metadata with tokens:
cat > ~/.sealos/auth.json << EOF
{"region":"$REGION","access_token":"$ACCESS_TOKEN","regional_token":"$REGIONAL_TOKEN","authenticated_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","auth_method":"oauth2_device_grant"}
EOF
chmod 600 ~/.sealos/auth.json4.3 Workspace Selection (every deploy)
After auth is confirmed, always let the user choose which workspace to deploy to. The last-used workspace is the default.
With Node.js:
node "<SKILL_DIR>/scripts/sealos-auth.mjs" listReturns:
{
"current": "ns-abc",
"workspaces": [
{ "uid": "...", "id": "ns-abc", "teamName": "My Team", "role": 0, "nstype": 1 },
{ "uid": "...", "id": "ns-def", "teamName": "Dev Team", "role": 0, "nstype": 0 },
{ "uid": "...", "id": "ns-ghi", "teamName": "Staging", "role": 2, "nstype": 0 }
]
}Present the workspace list to the user. Put the `current` workspace first, mark it as last used:
Which workspace do you want to deploy to?
1. ns-abc — My Team ← current
2. ns-def — Dev Team
3. ns-ghi — Staging
Default: ns-abc (My Team)Display format is id — teamName. The current field from the JSON indicates the last-used workspace — always list it first.
- If the user picks the same workspace as
current→ no action needed, kubeconfig is already valid. - If the user picks a different workspace → switch:
node "<SKILL_DIR>/scripts/sealos-auth.mjs" switch <ns-id>This updates ~/.sealos/kubeconfig and records the new workspace as current_workspace in auth.json for next time.
Without Node.js (curl fallback):
List workspaces:
NS_RESP=$(curl -ksf "$REGION/api/auth/namespace/list" \
-H "Authorization: $REGIONAL_TOKEN")Parse and present options to user. If the user picks a different workspace:
SWITCH_RESP=$(curl -ksf -X POST "$REGION/api/auth/namespace/switch" \
-H "Authorization: $REGIONAL_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"ns_uid\":\"$TARGET_UID\"}")
NEW_TOKEN=$(echo "$SWITCH_RESP" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)
# Get new kubeconfig
KC_RESP=$(curl -ksf "$REGION/api/auth/getKubeconfig" \
-H "Authorization: $NEW_TOKEN")
node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8')); process.stdout.write(d.data.kubeconfig)" <<< "$KC_RESP" > ~/.sealos/kubeconfig 2>/dev/null \
|| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['kubeconfig'])" <<< "$KC_RESP" > ~/.sealos/kubeconfig
chmod 600 ~/.sealos/kubeconfig
# Update auth.json with new token
REGIONAL_TOKEN="$NEW_TOKEN"If only one workspace exists, skip the selection prompt and use it directly.
Step 5: Ready
Only reach this section after:
- Step 1 environment detection/checks passed
- Step 2 capability classification completed
- Step 4 auth/workspace checks passed
- And only then Step 3 project context was collected
Report to user with a short readiness summary. This is a user-facing status snapshot, not a full artifact dump. Keep it focused on the key capabilities and blockers only.
Do not add a "full details" section in the default output.
Recommended format:
Project:
✓ <PROJECT.repo_name> (<PROJECT.work_dir>)
✓ git: <BRANCH> ← <GITHUB_URL or "local only">
✓ README: <one-line summary of what the project does>
Environment:
○ Docker <version> (or: ✗ Docker — local build path currently blocked)
✓ git <version>
○ Node.js <version> (or: ✗ Node.js — using AI fallback mode)
○ Python <version> (or: ✗ Python — template validation via AI)
○ kubectl <version> (or: ✗ kubectl — update/rollout path blocked)
○ gh <version> (or: ✗ gh CLI — local GHCR push path blocked)
Auth:
✓ Sealos Cloud (<region>)
✓ Workspace: <ns-id> (<teamName>)If Docker, gh, buildx, or registry connectivity are not ready, report them now as path-specific warnings. Only upgrade them to hard blockers if Phase 2/3 confirms that local build/push is required.
Output rules:
- Show only the high-signal items a user needs to decide whether to continue
- Do not print raw command output or exhaustive diagnostics in the normal summary
- If a capability is missing, explain briefly which later path it blocks
- Prefer one-line project identification plus compact Environment/Auth sections over long prose
Record ENV and PROJECT for subsequent phases → proceed to modules/pipeline.md.