Skip to content

Commit a3be0d3

Browse files
authored
feat(sealos-deploy): add template fast path detection (#49)
1 parent ce1a67e commit a3be0d3

10 files changed

Lines changed: 659 additions & 1 deletion

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env node
2+
3+
import assert from 'node:assert/strict'
4+
import { execFileSync } from 'node:child_process'
5+
import fs from 'node:fs'
6+
import os from 'node:os'
7+
import path from 'node:path'
8+
import test from 'node:test'
9+
import { fileURLToPath } from 'node:url'
10+
11+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
12+
const repoRoot = path.resolve(scriptDir, '..')
13+
const sourceSkillDir = path.join(repoRoot, 'skills', 'sealos-deploy')
14+
const repoUrl = 'https://github.com/usememos/memos'
15+
16+
const templateYaml = `apiVersion: app.sealos.io/v1
17+
kind: Template
18+
metadata:
19+
name: memos
20+
spec:
21+
title: Memos
22+
url: https://github.com/usememos/memos
23+
defaults:
24+
app_name:
25+
type: string
26+
value: memos
27+
inputs: {}
28+
appConfig:
29+
data:
30+
url: https://github.com/usememos/memos
31+
`
32+
33+
test('/sealos-deploy materializes a configured GitHub template fast path', () => {
34+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sealos-template-fast-path-'))
35+
const skillDir = path.join(tmpRoot, 'sealos-deploy')
36+
const workDir = path.join(tmpRoot, 'work')
37+
38+
try {
39+
fs.cpSync(sourceSkillDir, skillDir, { recursive: true })
40+
fs.mkdirSync(workDir, { recursive: true })
41+
42+
const configPath = path.join(skillDir, 'config.json')
43+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
44+
config.template_fast_path = {
45+
enabled: true,
46+
templates: [
47+
{
48+
name: 'memos',
49+
title: 'Memos',
50+
source_repos: [repoUrl],
51+
template_yaml: templateYaml,
52+
args: {
53+
default_app_name: 'memos',
54+
},
55+
},
56+
],
57+
}
58+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
59+
60+
execFileSync(process.execPath, [
61+
path.join(skillDir, 'scripts', 'detect-template.mjs'),
62+
'--github-url',
63+
repoUrl,
64+
'--work-dir',
65+
workDir,
66+
'--skill-dir',
67+
skillDir,
68+
], { encoding: 'utf8' })
69+
70+
const matchPath = path.join(workDir, '.sealos', 'template-match.json')
71+
const templatePath = path.join(workDir, '.sealos', 'template', 'index.yaml')
72+
73+
execFileSync(process.execPath, [
74+
path.join(skillDir, 'scripts', 'validate-artifacts.mjs'),
75+
'template-match',
76+
matchPath,
77+
], { encoding: 'utf8' })
78+
79+
const match = JSON.parse(fs.readFileSync(matchPath, 'utf8'))
80+
assert.equal(match.matched, true)
81+
assert.equal(match.materialized, true)
82+
assert.equal(match.repo.reference, 'usememos/memos')
83+
assert.equal(match.repo.github_url, repoUrl)
84+
assert.equal(match.template.name, 'memos')
85+
assert.equal(match.source, 'config')
86+
assert.equal(match.template_path, '.sealos/template/index.yaml')
87+
assert.deepEqual(match.args, { default_app_name: 'memos' })
88+
89+
const materializedTemplate = fs.readFileSync(templatePath, 'utf8')
90+
assert.match(materializedTemplate, /^apiVersion:\s*app\.sealos\.io\/v1$/m)
91+
assert.match(materializedTemplate, /^kind:\s*Template$/m)
92+
93+
assert.equal(fs.existsSync(path.join(workDir, '.sealos', 'analysis.json')), false)
94+
assert.equal(fs.existsSync(path.join(workDir, '.sealos', 'build')), false)
95+
96+
const pipeline = fs.readFileSync(path.join(sourceSkillDir, 'modules', 'pipeline.md'), 'utf8')
97+
assert.match(
98+
pipeline,
99+
/`matched=true` and `materialized=true` skip Phase 1 through Phase 5/,
100+
)
101+
} finally {
102+
fs.rmSync(tmpRoot, { recursive: true, force: true })
103+
}
104+
})

skills/sealos-deploy/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Located in `scripts/` within this skill directory (`<SKILL_DIR>/scripts/`):
138138
| Script | Usage | Purpose |
139139
|--------|-------|---------|
140140
| `score-model.mjs` | `node score-model.mjs <repo-dir>` | Deterministic readiness scoring (0-12) |
141+
| `detect-template.mjs` | `node detect-template.mjs [--github-url <url>] --work-dir <repo-dir> --skill-dir <SKILL_DIR>` | Detect configured GitHub repo → Sealos template fast-path matches |
141142
| `validate-artifacts.mjs` | `node validate-artifacts.mjs --dir <work-dir>` | Validate `.sealos` JSON artifacts against enforced schemas |
142143
| `detect-image.mjs` | `node detect-image.mjs <github-url> [work-dir]` or `node detect-image.mjs <work-dir>` | Detect existing Docker/GHCR images |
143144
| `build-push.mjs` | `node build-push.mjs <work-dir> <repo> [--registry ghcr\|dockerhub] [--user <user>]` | Build amd64 image & push to the selected registry (Docker Hub path assumes a public image at deploy time; omitting `--registry` keeps auto-detect behavior) |
@@ -176,6 +177,7 @@ Paths used in pipeline.md follow the pattern:
176177
| Phase | Action | Skip When |
177178
|-------|--------|-----------|
178179
| 0 — Preflight | Capability scan, path-specific warnings, Sealos auth | Initial blockers resolved |
180+
| 0.5 — Template Fast Path | Match GitHub repo to a configured Sealos template | No match, or match cannot materialize template YAML |
179181
| 1 — Assess | Clone repo (or use current project), analyze deployability | Score too low → stop |
180182
| 2 — Detect | Find existing image (Docker Hub / GHCR / README) | Found → jump to Phase 5 |
181183
| 3 — Dockerfile | Generate Dockerfile if missing | Already has one → skip |
@@ -194,6 +196,11 @@ Input (GitHub URL / local path)
194196
[Phase 0] Preflight ── fail → guide user to fix and STOP
195197
│ pass
196198
199+
[Phase 0.5] Template fast path
200+
201+
├── materialized template match ───────┐
202+
│ │
203+
▼ │
197204
[Phase 1] Assess ── not suitable → STOP with reason
198205
│ suitable
199206
@@ -211,6 +218,7 @@ Input (GitHub URL / local path)
211218
212219
213220
[Phase 5] Generate Sealos Template
221+
◄──────────────────────────────────────┘
214222
215223
216224
[Phase 5.5] Configure ── present env vars → ask user for inputs → confirm

skills/sealos-deploy/config.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,9 @@
66
"https://gzg.sealos.run",
77
"https://bja.sealos.run",
88
"https://hzh.sealos.run"
9-
]
9+
],
10+
"template_fast_path": {
11+
"enabled": true,
12+
"templates": []
13+
}
1014
}

skills/sealos-deploy/evals/evals.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@
121121
{"name": "identifies-web-service", "description": "Identifies this as a web application on port 8080"},
122122
{"name": "produces-structured-report", "description": "Output contains a structured assessment report"}
123123
]
124+
},
125+
{
126+
"id": 10,
127+
"prompt": "/sealos-deploy https://github.com/toeverything/affine",
128+
"expected_output": "Runs Phase 0.5 template fast-path detection before source analysis; if no configured template exists, writes template-match.json with matched=false and continues the normal deploy pipeline",
129+
"files": [],
130+
"assertions": [
131+
{"name": "runs-template-fast-path", "description": "Checks configured GitHub repo to Sealos template mappings before Phase 1 assessment"},
132+
{"name": "writes-template-match", "description": "Writes .sealos/template-match.json for both matched and unmatched outcomes"},
133+
{"name": "does-not-false-positive", "description": "Does not skip source analysis/build/template generation unless a matched template is materialized"}
134+
]
124135
}
125136
]
126137
}

skills/sealos-deploy/modules/pipeline.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
After preflight passes, execute Phase 1–6 in order.
44

5+
Before Phase 1, execute Phase 0.5 Template Fast Path when a GitHub repository reference is available.
6+
57
`SKILL_DIR` refers to the directory containing this skill's SKILL.md. Sibling skills are at `<SKILL_DIR>/../`.
68

79
Use `ENV` from preflight to choose between script mode (Node.js available) and fallback mode (AI-native).
@@ -13,6 +15,7 @@ All pipeline outputs are written under `.sealos/` in `WORK_DIR`:
1315
```
1416
<WORK_DIR>/.sealos/
1517
├── config.json ← user configuration overrides (manual, committed to git)
18+
├── template-match.json ← Phase 0.5 template fast-path decision
1619
├── state.json ← deployment state (auto-maintained after Phase 6)
1720
├── analysis.json ← project analysis snapshot (regenerated each deploy)
1821
├── build/ ← created only if Phase 4 actually runs
@@ -30,6 +33,7 @@ All pipeline outputs are written under `.sealos/` in `WORK_DIR`:
3033

3134
JSON artifacts under `.sealos/` are governed by explicit schemas in `<SKILL_DIR>/schemas/`:
3235
- `config.schema.json`
36+
- `template-match.schema.json`
3337
- `analysis.schema.json`
3438
- `build-result.schema.json`
3539
- `state.schema.json`
@@ -176,6 +180,46 @@ If restart → remove `.sealos/analysis.json`, `.sealos/build/`, `.sealos/templa
176180

177181
---
178182

183+
## Phase 0.5: Template Fast Path
184+
185+
Run this phase after preflight has resolved `WORK_DIR`, `GITHUB_URL`, and `REPO_NAME`, and before Phase 1 assessment.
186+
187+
The goal is to avoid source analysis, Dockerfile generation, image builds, and template generation for repositories that are already represented by a known Sealos template.
188+
189+
With Node.js:
190+
191+
```bash
192+
node "<SKILL_DIR>/scripts/detect-template.mjs" \
193+
--github-url "$GITHUB_URL" \
194+
--work-dir "$WORK_DIR" \
195+
--skill-dir "<SKILL_DIR>"
196+
```
197+
198+
The script writes `.sealos/template-match.json` every time it runs.
199+
200+
Decision:
201+
202+
- `matched=false` → continue to Phase 1 normally.
203+
- `matched=true` and `materialized=false` → report the matched template name and continue to Phase 1 normally; this is only a recommendation because no deployable template YAML was available.
204+
- `matched=true` and `materialized=true` → skip Phase 1 through Phase 5 because `.sealos/template/index.yaml` already exists. Continue with Phase 5.5 configuration and Phase 6 deployment.
205+
206+
The fast path is configured in `<SKILL_DIR>/config.json` under `template_fast_path.templates`. A template entry must include `name` and `source_repos`; it materializes only when it also provides valid Sealos Template YAML through one of:
207+
208+
- `template_yaml`
209+
- `template_path`
210+
- `template_url`
211+
212+
Template YAML must include:
213+
214+
```yaml
215+
apiVersion: app.sealos.io/v1
216+
kind: Template
217+
```
218+
219+
If a matched entry cannot materialize YAML, do not treat it as a deployable result and do not skip build or template generation.
220+
221+
---
222+
179223
## Phase 1: Assess
180224
181225
`WORK_DIR`, `GITHUB_URL`, `REPO_NAME`, and README context are already resolved in preflight (Step 2).

skills/sealos-deploy/modules/preflight.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,12 @@ At the end of preflight, explicitly tell the user:
171171
- which items are ready
172172
- which items are warnings only
173173
- which later path each warning would block
174+
- whether Phase 0.5 template fast path can run from the resolved GitHub repo metadata
174175

175176
Example:
176177
- "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."
177178
- "`kubectl` is missing. Fresh deploy can continue, but UPDATE mode and rollout verification will be blocked until it is installed."
179+
- "Template fast path will check configured GitHub repo → Sealos template mappings before source analysis."
178180

179181
## Step 3: Project Context
180182

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://skills.sh/sealos-deploy/schemas/template-match.schema.json",
4+
"title": "Sealos Deploy Template Match Artifact",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"required": [
8+
"version",
9+
"generated_at",
10+
"matched",
11+
"materialized",
12+
"repo",
13+
"template",
14+
"args",
15+
"source",
16+
"template_path",
17+
"reason"
18+
],
19+
"properties": {
20+
"version": {
21+
"type": "string",
22+
"const": "1.0"
23+
},
24+
"generated_at": {
25+
"type": "string",
26+
"format": "date-time"
27+
},
28+
"matched": {
29+
"type": "boolean"
30+
},
31+
"materialized": {
32+
"type": "boolean"
33+
},
34+
"repo": {
35+
"anyOf": [
36+
{
37+
"type": "object",
38+
"additionalProperties": false,
39+
"required": [
40+
"reference",
41+
"github_url"
42+
],
43+
"properties": {
44+
"reference": {
45+
"type": "string",
46+
"minLength": 1
47+
},
48+
"github_url": {
49+
"type": "string",
50+
"minLength": 1
51+
}
52+
}
53+
},
54+
{
55+
"type": "null"
56+
}
57+
]
58+
},
59+
"template": {
60+
"anyOf": [
61+
{
62+
"type": "object",
63+
"additionalProperties": false,
64+
"required": [
65+
"name",
66+
"title"
67+
],
68+
"properties": {
69+
"name": {
70+
"type": "string",
71+
"minLength": 1
72+
},
73+
"title": {
74+
"type": "string",
75+
"minLength": 1
76+
},
77+
"description": {
78+
"type": "string"
79+
}
80+
}
81+
},
82+
{
83+
"type": "null"
84+
}
85+
]
86+
},
87+
"args": {
88+
"type": "object",
89+
"additionalProperties": {
90+
"type": "string"
91+
}
92+
},
93+
"source": {
94+
"type": "string",
95+
"enum": [
96+
"config",
97+
"catalog",
98+
"none"
99+
]
100+
},
101+
"template_path": {
102+
"anyOf": [
103+
{
104+
"type": "string",
105+
"const": ".sealos/template/index.yaml"
106+
},
107+
{
108+
"type": "null"
109+
}
110+
]
111+
},
112+
"reason": {
113+
"type": "string",
114+
"minLength": 1
115+
}
116+
}
117+
}

skills/sealos-deploy/scripts/artifact-validator.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const SCHEMA_FILES = {
1212
analysis: 'analysis.schema.json',
1313
'build-result': 'build-result.schema.json',
1414
state: 'state.schema.json',
15+
'template-match': 'template-match.schema.json',
1516
}
1617

1718
function isPlainObject(value) {
@@ -321,6 +322,7 @@ const SEMANTIC_VALIDATORS = {
321322
analysis: validateAnalysisSemantics,
322323
'build-result': validateBuildResultSemantics,
323324
state: validateStateSemantics,
325+
'template-match': () => {},
324326
}
325327

326328
export function inferArtifactKind(filePath) {
@@ -334,6 +336,8 @@ export function inferArtifactKind(filePath) {
334336
return 'build-result'
335337
case 'state.json':
336338
return 'state'
339+
case 'template-match.json':
340+
return 'template-match'
337341
default:
338342
return null
339343
}

0 commit comments

Comments
 (0)