@rashidee/co2 1.3.16 → 1.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.co2-dat/app.db-shm +0 -0
- package/dist/.co2-dat/app.db-wal +0 -0
- package/dist/index.js +5 -1
- package/drizzle/0020_cicd_guard_token.sql +4 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +1 -1
- package/plugin/skills/util-gencicdscript/SKILL.md +13 -19
- package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +23 -90
- package/plugin/skills/util-plancicd/SKILL.md +305 -310
package/dist/.co2-dat/app.db-shm
CHANGED
|
Binary file
|
package/dist/.co2-dat/app.db-wal
CHANGED
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -6809,7 +6809,7 @@ var {
|
|
|
6809
6809
|
// package.json
|
|
6810
6810
|
var package_default = {
|
|
6811
6811
|
name: "@rashidee/co2",
|
|
6812
|
-
version: "1.3.
|
|
6812
|
+
version: "1.3.18",
|
|
6813
6813
|
description: "Compound Context Studio \u2014 self-hosted team context authoring for CO2 projects",
|
|
6814
6814
|
type: "module",
|
|
6815
6815
|
license: "MIT",
|
|
@@ -14735,6 +14735,10 @@ var cicdSetups = sqliteTable(
|
|
|
14735
14735
|
// project_configs.updatedAt at generation
|
|
14736
14736
|
appPort: integer("app_port"),
|
|
14737
14737
|
// last spawned port of the generated app
|
|
14738
|
+
// Guard token issued to the last spawned app (also kept in memory). Persisted so an app
|
|
14739
|
+
// that outlives a studio restart still passes /api/cicd-guard — otherwise its develop
|
|
14740
|
+
// gate reads "studio unreachable" and every control stays disabled until a manual bounce.
|
|
14741
|
+
guardToken: text("guard_token"),
|
|
14738
14742
|
createdBy: integer("created_by").notNull(),
|
|
14739
14743
|
// soft ref → users.id
|
|
14740
14744
|
createdAt: text("created_at").notNull(),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- v2.3.1: Persist the generated CI/CD app's guard token. The token was in-memory only, so a
|
|
2
|
+
-- studio restart orphaned a still-running app: its spawn-env token no longer validated against
|
|
3
|
+
-- /api/cicd-guard, the develop gate read "studio unreachable" and every control stayed disabled.
|
|
4
|
+
ALTER TABLE `cicd_setups` ADD `guard_token` text;
|
|
@@ -141,6 +141,13 @@
|
|
|
141
141
|
"when": 1785000000000,
|
|
142
142
|
"tag": "0019_git_provider",
|
|
143
143
|
"breakpoints": true
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"idx": 20,
|
|
147
|
+
"version": "6",
|
|
148
|
+
"when": 1785100000000,
|
|
149
|
+
"tag": "0020_cicd_guard_token",
|
|
150
|
+
"breakpoints": true
|
|
144
151
|
}
|
|
145
152
|
]
|
|
146
|
-
}
|
|
153
|
+
}
|
package/package.json
CHANGED
|
@@ -21,12 +21,12 @@ description: >
|
|
|
21
21
|
copy, docker run or kubectl apply, or none). Controls are disabled for any application that
|
|
22
22
|
has never completed a conductor-develop workflow run (checked against the studio's
|
|
23
23
|
/api/cicd-guard endpoint using the spawn-env token). Health/port probing marks apps already
|
|
24
|
-
running — even ones started outside this app.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
running — even ones started outside this app. There is NO version selector and there are NO
|
|
25
|
+
Build All / Start All / Stop All buttons — every control operates per application on the
|
|
26
|
+
current working tree, and the plan's commands run verbatim (no {VERSION} or other token
|
|
27
|
+
substitution) — docker images are tagged <repo>/<app>:latest when the plan's Docker section
|
|
28
|
+
is active (managed Dockerfiles materialized before Build when missing; Build never
|
|
29
|
+
pushes). When the approved plan declares an `## Infrastructure` list, the
|
|
30
30
|
main page shows an "Infrastructure" link that opens a separate status-only page listing every
|
|
31
31
|
supporting 3rd-party service with a live TCP-reachability indicator and a console link. Writes cicd/CICD_GENERATION.md with `**Status**:` IN PROGRESS →
|
|
32
32
|
COMPLETED so the studio watcher detects completion. Idempotent — re-running regenerates
|
|
@@ -75,8 +75,8 @@ Read `references/cicd-app-template.md` for the complete file templates. Produce:
|
|
|
75
75
|
`{ "path": "Dockerfile", "content": "<body>" }` for a `managed` Dockerfile from the plan's
|
|
76
76
|
`## Dockerfiles` section, or `{ "path": "Dockerfile", "content": null }` for an `existing`
|
|
77
77
|
one (`null` entirely when the app has no image). Copy managed Dockerfile bodies verbatim.
|
|
78
|
-
Commands
|
|
79
|
-
|
|
78
|
+
Commands are copied verbatim from the plan and run as-is — there is NO `{VERSION}` token
|
|
79
|
+
or any other placeholder substitution. `deployMethod` is the plan's Deploy Method column
|
|
80
80
|
(`none|web-server|docker|kubernetes`) and `deploy` its command (`null` when the method is
|
|
81
81
|
`none`). The `infra` array comes from the plan's `## Infrastructure` table (one entry per
|
|
82
82
|
row); `url` is `null` when the Console URL cell is `-`. Emit `"infra": []` when the plan's
|
|
@@ -163,21 +163,15 @@ port, application count) and STOP — the studio starts/embeds the app.
|
|
|
163
163
|
(`<project>/<folder>`) as working directory — command paths are relative to the application
|
|
164
164
|
folder (e.g. `java -jar target\app.jar`), never to the project root. Deploy renders only
|
|
165
165
|
when the plan assigned a deploy method other than `none`.
|
|
166
|
-
- **
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
semantics: the code built is always the working tree — the selector never checks out code.
|
|
166
|
+
- **NO version UI, NO All-apps buttons, NO command substitution**: the page carries NO
|
|
167
|
+
version dropdown or any other version choice, and NO Build All / Start All / Stop All
|
|
168
|
+
buttons — Build/Start/Stop/Deploy are per-application only and always operate on the
|
|
169
|
+
CURRENT working tree, running the plan's commands verbatim (no `{VERSION}` or other token
|
|
170
|
+
substitution). Never render a version control and never check out code.
|
|
172
171
|
- **Dockerfile materialization**: before Build, when an app's `dockerfile.content` is set and
|
|
173
172
|
no file exists at its path in the application folder, the server writes it (never
|
|
174
173
|
overwriting an existing Dockerfile — mirror of the managed-config rule). Build never
|
|
175
174
|
pushes an image; pushing belongs to a `docker` deploy command.
|
|
176
|
-
- **All-apps controls**: the top bar carries **Build All**, **Start All** and **Stop All**
|
|
177
|
-
buttons. Build All builds eligible apps SEQUENTIALLY (one at a time — honest logs, no CPU
|
|
178
|
-
storm); Start All starts every eligible stopped app; Stop All stops every running app.
|
|
179
|
-
Eligible = develop-gate not failed, not busy, and (for build/start) not already running;
|
|
180
|
-
ineligible apps are skipped silently — per-tab badges show progress.
|
|
181
175
|
- Develop gate: controls stay disabled for an application that never completed a
|
|
182
176
|
conductor-develop run — checked via `${CO2_STUDIO_URL}/api/cicd-guard/${CO2_PROJECT_ID}`
|
|
183
177
|
with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
|
|
@@ -66,7 +66,6 @@ function currentBranch() {
|
|
|
66
66
|
// its tab controls are disabled. Refreshed every 30 s; the last known answer is kept when the
|
|
67
67
|
// studio is briefly unreachable, and `null` (never fetched) disables controls with a hint.
|
|
68
68
|
let guardApps = null // Map lower(name|folder) -> developedOnce
|
|
69
|
-
let guardVersions = [] // project versions (semver strings, studio order — latest first)
|
|
70
69
|
async function refreshGuard() {
|
|
71
70
|
if (!PROJECT_ID) return
|
|
72
71
|
try {
|
|
@@ -77,8 +76,6 @@ async function refreshGuard() {
|
|
|
77
76
|
guardApps = new Map()
|
|
78
77
|
for (const a of g.applications) guardApps.set(String(a.name).toLowerCase(), !!a.developedOnce)
|
|
79
78
|
}
|
|
80
|
-
// The topbar version selector's choices — the {VERSION} tag substituted into commands.
|
|
81
|
-
if (Array.isArray(g.versions)) guardVersions = g.versions.map((v) => String(v.semver)).filter(Boolean)
|
|
82
79
|
} catch {}
|
|
83
80
|
}
|
|
84
81
|
function developedOnce(app) {
|
|
@@ -229,13 +226,6 @@ function tcpProbe(host, port, timeout = 1000) {
|
|
|
229
226
|
})
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
// {VERSION} token substitution — the version comes from the page's topbar selector (the
|
|
233
|
-
// studio guard's versions, latest preselected). Tag-only: never checks out code. The `v`
|
|
234
|
-
// prefix is stripped so docker tags read `1.2.0`, not `v1.2.0`.
|
|
235
|
-
function subst(cmd, version) {
|
|
236
|
-
return String(cmd || '').replaceAll('{VERSION}', String(version || 'latest').replace(/^v/, ''))
|
|
237
|
-
}
|
|
238
|
-
|
|
239
229
|
// Managed Dockerfile from the plan's ## Dockerfiles — written before Build only when the
|
|
240
230
|
// application folder has none (an existing Dockerfile is never overwritten).
|
|
241
231
|
function ensureDockerfile(app) {
|
|
@@ -249,38 +239,37 @@ function ensureDockerfile(app) {
|
|
|
249
239
|
} catch (e) { logLine(app, `dockerfile: FAILED to write — ${e.message}`) }
|
|
250
240
|
}
|
|
251
241
|
|
|
252
|
-
function buildApp(app
|
|
253
|
-
if (activity.has(app.name))
|
|
242
|
+
function buildApp(app) {
|
|
243
|
+
if (activity.has(app.name)) return
|
|
254
244
|
activity.set(app.name, 'building'); lastError.delete(app.name)
|
|
255
245
|
ensureConfigs(app)
|
|
256
246
|
ensureDockerfile(app)
|
|
257
|
-
runLogged(app,
|
|
247
|
+
runLogged(app, app.build, (code) => {
|
|
258
248
|
activity.delete(app.name)
|
|
259
249
|
if (code !== 0) lastError.set(app.name, `build exited ${code} — see log`)
|
|
260
250
|
else logLine(app, 'build OK')
|
|
261
|
-
onDone && onDone()
|
|
262
251
|
})
|
|
263
252
|
}
|
|
264
253
|
|
|
265
|
-
function deployApp(app
|
|
254
|
+
function deployApp(app) {
|
|
266
255
|
if (activity.has(app.name) || !app.deploy) return
|
|
267
256
|
activity.set(app.name, 'deploying'); lastError.delete(app.name)
|
|
268
|
-
runLogged(app,
|
|
257
|
+
runLogged(app, app.deploy, (code) => {
|
|
269
258
|
activity.delete(app.name)
|
|
270
259
|
if (code !== 0) lastError.set(app.name, `deploy exited ${code} — see log`)
|
|
271
260
|
else logLine(app, `deploy OK (${app.deployMethod})`)
|
|
272
261
|
})
|
|
273
262
|
}
|
|
274
263
|
|
|
275
|
-
function startApp(app
|
|
264
|
+
function startApp(app) {
|
|
276
265
|
if (running.get(app.name) || activity.has(app.name)) return
|
|
277
266
|
failed.delete(app.name); lastError.delete(app.name)
|
|
278
267
|
ensureConfigs(app)
|
|
279
268
|
if (app.method === 'container') {
|
|
280
|
-
runLogged(app,
|
|
269
|
+
runLogged(app, app.start, () => {})
|
|
281
270
|
saveState(); return
|
|
282
271
|
}
|
|
283
|
-
const child = runLogged(app,
|
|
272
|
+
const child = runLogged(app, app.start, (code) => {
|
|
284
273
|
running.delete(app.name)
|
|
285
274
|
// A user-requested Stop exits nonzero (killed) — that is not a failure.
|
|
286
275
|
if (code !== 0 && !stopping.delete(app.name)) {
|
|
@@ -327,9 +316,9 @@ function killByPort(port) {
|
|
|
327
316
|
} catch { return false }
|
|
328
317
|
}
|
|
329
318
|
|
|
330
|
-
function stopApp(app
|
|
319
|
+
function stopApp(app) {
|
|
331
320
|
if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
|
|
332
|
-
const stopCmd =
|
|
321
|
+
const stopCmd = app.stop
|
|
333
322
|
try { execFileSync(stopCmd.split(' ')[0], stopCmd.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
|
|
334
323
|
} else {
|
|
335
324
|
const child = running.get(app.name)
|
|
@@ -368,7 +357,6 @@ body{font-family:system-ui,sans-serif;margin:0;background:#f6f7f9;color:#1c2128;
|
|
|
368
357
|
.badge.running{background:#1c8a4a}.badge.failed{background:#c23b3b}.badge.building,.badge.deploying{background:#c9922a}
|
|
369
358
|
.url{font-size:12px;color:#1a66d0;text-decoration:underline;word-break:break-all}
|
|
370
359
|
button{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer}
|
|
371
|
-
select{background:#fff;color:#1c2128;border:1px solid #c6cdd6;border-radius:6px;padding:3px 6px;font-size:12px}
|
|
372
360
|
button:hover:enabled{background:#eef1f5}
|
|
373
361
|
button:disabled{opacity:.4;cursor:default}
|
|
374
362
|
.notice{padding:4px 12px;font-size:11px;color:#8a5a00;background:#fff7e6;display:none}
|
|
@@ -389,10 +377,6 @@ button:disabled{opacity:.4;cursor:default}
|
|
|
389
377
|
.cfg pre{background:#0f1216;color:#d7dde4;font-size:11px;padding:8px 10px;border-radius:6px;overflow:auto;max-height:320px;white-space:pre-wrap;margin:4px 0 0}
|
|
390
378
|
</style></head><body>
|
|
391
379
|
<div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span>
|
|
392
|
-
<span class="meta">version</span><select id="ver" title="Tag substituted into {VERSION} in commands (docker image tag)"><option value="">latest</option></select>
|
|
393
|
-
<button onclick="actAll('build')" title="Build every eligible application, one at a time">Build All</button>
|
|
394
|
-
<button onclick="actAll('start')" title="Start every eligible stopped application">Start All</button>
|
|
395
|
-
<button onclick="actAll('stop')" title="Stop every running application">Stop All</button>
|
|
396
380
|
<span style="flex:1"></span><a id="infralink" class="url" href="infra" target="_blank" rel="noreferrer" style="display:none">Infrastructure ↗</a></div>
|
|
397
381
|
<div class="tabs" id="tabs"></div>
|
|
398
382
|
<div class="bar" id="bar"></div>
|
|
@@ -431,7 +415,7 @@ function renderBar(){
|
|
|
431
415
|
<a class="url" href="\${url}" target="_blank" rel="noreferrer">\${url}</a>
|
|
432
416
|
<span style="flex:1"></span>
|
|
433
417
|
<button onclick="openConfig()" title="Show this application's runtime configuration files">Config</button>
|
|
434
|
-
<button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''} title="\${a.image?'Builds the app AND the docker image '+a.image+'
|
|
418
|
+
<button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''} title="\${a.image?'Builds the app AND the docker image '+a.image+':latest':'Runs the plan build command'}">Build</button>
|
|
435
419
|
<button onclick="act('start')" \${!gate||busy||a.phase==='running'?'disabled':''}>Start</button>
|
|
436
420
|
<button onclick="act('stop')" \${a.phase!=='running'?'disabled':''}>Stop</button>
|
|
437
421
|
\${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
|
|
@@ -461,7 +445,10 @@ function setActive(name){
|
|
|
461
445
|
function openWs(name){
|
|
462
446
|
// Path-relative + scheme-aware so the stream works direct (:5000/) AND behind the studio's
|
|
463
447
|
// /api/cicd/<id>/app-proxy/ prefix (which tunnels the upgrade), also under HTTPS.
|
|
464
|
-
|
|
448
|
+
// NO backslash/regex here on purpose: this string is inside server.js's PAGE template literal,
|
|
449
|
+
// where a client-side regex like /\/$/ cooks to //$/ (a line comment) and kills the script.
|
|
450
|
+
var base = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname
|
|
451
|
+
ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+base+'/ws?app='+encodeURIComponent(name))
|
|
465
452
|
ws.onmessage = e => appendLog(e.data)
|
|
466
453
|
ws.onclose = () => { ws = null; if (active === name) wsTimer = setTimeout(() => openWs(name), 2000) }
|
|
467
454
|
}
|
|
@@ -501,35 +488,17 @@ async function openConfig(){
|
|
|
501
488
|
function closeConfig(){ document.getElementById('cfgModal').classList.remove('open') }
|
|
502
489
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeConfig() })
|
|
503
490
|
|
|
504
|
-
// Topbar version selector — the studio guard's project versions (latest first). Repopulated
|
|
505
|
-
// only when the list changes so the user's selection survives the 3 s poll.
|
|
506
|
-
let verChoices = ''
|
|
507
|
-
function renderVersions(versions){
|
|
508
|
-
const sel = document.getElementById('ver')
|
|
509
|
-
const list = (versions && versions.length ? versions : []).join('|')
|
|
510
|
-
if (list === verChoices) return
|
|
511
|
-
verChoices = list
|
|
512
|
-
sel.innerHTML = (versions && versions.length ? versions : ['latest'])
|
|
513
|
-
.map((v, i) => '<option value="'+esc(v)+'"'+(i===0?' selected':'')+'>'+esc(v)+'</option>').join('')
|
|
514
|
-
}
|
|
515
|
-
function selectedVersion(){ return document.getElementById('ver').value || '' }
|
|
516
|
-
|
|
517
491
|
async function refresh(){
|
|
518
492
|
const s = await fetch('api/state').then(r => r.json()).catch(() => null)
|
|
519
493
|
if (!s) return
|
|
520
494
|
state = s
|
|
521
495
|
document.getElementById('branch').textContent = s.currentBranch ? 'branch: '+s.currentBranch : ''
|
|
522
496
|
document.getElementById('infralink').style.display = s.hasInfra ? 'inline' : 'none'
|
|
523
|
-
renderVersions(s.versions)
|
|
524
497
|
if (!active && s.apps.length) setActive(s.apps[0].name)
|
|
525
498
|
renderTabs(); renderBar()
|
|
526
499
|
}
|
|
527
500
|
async function act(op){
|
|
528
|
-
await fetch('api/apps/'+encodeURIComponent(active)+'/'+op
|
|
529
|
-
refresh()
|
|
530
|
-
}
|
|
531
|
-
async function actAll(op){
|
|
532
|
-
await fetch('api/all/'+op+'?version='+encodeURIComponent(selectedVersion()), { method: 'POST' })
|
|
501
|
+
await fetch('api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
|
|
533
502
|
refresh()
|
|
534
503
|
}
|
|
535
504
|
// Rescan immediately when the user returns to this page (tab focus / regained visibility), so
|
|
@@ -614,7 +583,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
614
583
|
return json(res, 200, {
|
|
615
584
|
currentBranch: currentBranch(),
|
|
616
585
|
hasInfra: (loadConfig().infra || []).length > 0,
|
|
617
|
-
versions: guardVersions,
|
|
618
586
|
apps,
|
|
619
587
|
})
|
|
620
588
|
}
|
|
@@ -624,10 +592,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
624
592
|
if (!app) return json(res, 404, { error: 'unknown app' })
|
|
625
593
|
return json(res, 200, { configs: (app.configs || []).map((c) => readConfig(app, c)) })
|
|
626
594
|
}
|
|
627
|
-
// ?version=<semver> on every action — the topbar selection, substituted into {VERSION}.
|
|
628
|
-
const reqVersion = (() => {
|
|
629
|
-
try { return new URL(req.url, 'http://x').searchParams.get('version') || '' } catch { return '' }
|
|
630
|
-
})()
|
|
631
595
|
const path = (req.url || '').split('?')[0]
|
|
632
596
|
const m = path.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
|
|
633
597
|
if (m && req.method === 'POST') {
|
|
@@ -636,42 +600,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
636
600
|
if (m[2] !== 'stop' && developedOnce(app) === false) {
|
|
637
601
|
return json(res, 409, { error: 'application has not completed a develop workflow run' })
|
|
638
602
|
}
|
|
639
|
-
if (m[2] === 'build') buildApp(app
|
|
640
|
-
else if (m[2] === 'start') startApp(app
|
|
641
|
-
else if (m[2] === 'deploy') deployApp(app
|
|
642
|
-
else stopApp(app
|
|
603
|
+
if (m[2] === 'build') buildApp(app)
|
|
604
|
+
else if (m[2] === 'start') startApp(app)
|
|
605
|
+
else if (m[2] === 'deploy') deployApp(app)
|
|
606
|
+
else stopApp(app)
|
|
643
607
|
return json(res, 200, { ok: true })
|
|
644
608
|
}
|
|
645
|
-
// All-apps controls. Eligible = develop gate not failed and not mid-build/deploy; build and
|
|
646
|
-
// start additionally skip apps already running. Builds run SEQUENTIALLY (honest logs, no
|
|
647
|
-
// CPU storm); start/stop fire together. Ineligible apps are skipped silently.
|
|
648
|
-
const all = path.match(/^\/api\/all\/(build|start|stop)$/)
|
|
649
|
-
if (all && req.method === 'POST') {
|
|
650
|
-
const op = all[1]
|
|
651
|
-
const apps = loadConfig().apps
|
|
652
|
-
if (op === 'stop') {
|
|
653
|
-
for (const a of apps) {
|
|
654
|
-
if (running.has(a.name)) stopApp(a, reqVersion)
|
|
655
|
-
else probe(a).then((up) => { if (up) stopApp(a, reqVersion) })
|
|
656
|
-
}
|
|
657
|
-
return json(res, 200, { ok: true })
|
|
658
|
-
}
|
|
659
|
-
const eligible = []
|
|
660
|
-
for (const a of apps) {
|
|
661
|
-
if (developedOnce(a) === false || activity.has(a.name)) continue
|
|
662
|
-
if (await probe(a)) continue // already running — skip for build AND start
|
|
663
|
-
eligible.push(a)
|
|
664
|
-
}
|
|
665
|
-
if (op === 'start') {
|
|
666
|
-
for (const a of eligible) startApp(a, reqVersion)
|
|
667
|
-
return json(res, 200, { ok: true, count: eligible.length })
|
|
668
|
-
}
|
|
669
|
-
// op === 'build' — sequential chain: each build starts when the previous one exits.
|
|
670
|
-
const queue = [...eligible]
|
|
671
|
-
const next = () => { const a = queue.shift(); if (a) buildApp(a, reqVersion, next) }
|
|
672
|
-
next()
|
|
673
|
-
return json(res, 200, { ok: true, count: eligible.length })
|
|
674
|
-
}
|
|
675
609
|
json(res, 404, { error: 'not found' })
|
|
676
610
|
})
|
|
677
611
|
|
|
@@ -718,9 +652,8 @@ Docker fields (from the plan's `## Docker` / `## Dockerfiles` sections): top-lev
|
|
|
718
652
|
`"dockerfile"` is `{ "path": "Dockerfile", "content": "<managed body>" }` for a `managed`
|
|
719
653
|
Dockerfile (materialized before Build when missing — never overwriting), or
|
|
720
654
|
`{ "path": "Dockerfile", "content": null }` for an `existing` one (`null` entirely when the
|
|
721
|
-
app has no image). Commands
|
|
722
|
-
|
|
723
|
-
execution time.
|
|
655
|
+
app has no image). Commands are copied verbatim from the plan and run as-is — no `{VERSION}`
|
|
656
|
+
or any other token substitution.
|
|
724
657
|
|
|
725
658
|
Each app also carries a `configs` array — the runtime config files the application reads at start,
|
|
726
659
|
from the plan's `## Application Configs` section (one entry per file). Fields:
|
|
@@ -743,7 +676,7 @@ verbatim (this is a local dev CI/CD app).
|
|
|
743
676
|
"folder": "my-app",
|
|
744
677
|
"method": "process",
|
|
745
678
|
"port": 3000,
|
|
746
|
-
"build": "pnpm install && pnpm build && docker build -t registry.example.com/team/my-app:
|
|
679
|
+
"build": "pnpm install && pnpm build && docker build -t registry.example.com/team/my-app:latest .",
|
|
747
680
|
"start": "node dist/index.js",
|
|
748
681
|
"stop": "process-kill",
|
|
749
682
|
"deployMethod": "kubernetes",
|
|
@@ -1,310 +1,305 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: util-plancicd
|
|
3
|
-
model: claude-opus-4-8
|
|
4
|
-
effort: high
|
|
5
|
-
description: >
|
|
6
|
-
Infer a local-deployment plan for every custom application of a CO2 project and write it to
|
|
7
|
-
cicd/CICD_PLAN.md for human review in Compound Context Studio. Reads the project's
|
|
8
|
-
ENVIRONMENT.md and DEVTOOL.md, CLAUDE.md (# Custom Applications, # Port Allocation) and each
|
|
9
|
-
application's context/specification/SPECIFICATION.md, then decides per application: deployment
|
|
10
|
-
method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
|
|
11
|
-
start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
|
|
12
|
-
kubectl apply — only when the environment calls for one), port and health-check URL, plus a
|
|
13
|
-
port for the generated CI/CD app itself. When Docker AND a docker repo/registry are both
|
|
14
|
-
configured (DEVTOOL.md + ENVIRONMENT.md), it additionally plans the docker-image build path:
|
|
15
|
-
per-application image names under the repo, Build commands extended with
|
|
16
|
-
`docker build -t <image>:
|
|
17
|
-
|
|
18
|
-
`## Dockerfiles` section for any dockerized application lacking one (materialized before
|
|
19
|
-
Build, never overwriting an existing file). It also infers each application's RUNTIME CONFIG — the
|
|
20
|
-
config files its start command reads (e.g. `.env`), deciding per file whether the values come
|
|
21
|
-
from ENVIRONMENT.md or from an existing `.env` / `.env-*` file already in the app folder, listing
|
|
22
|
-
the required keys and, for ENVIRONMENT.md-sourced files, the resolved content — so the generated
|
|
23
|
-
CI/CD app can complete the config before start (guaranteeing the app comes up) and show it in a
|
|
24
|
-
per-application Config modal for troubleshooting. It also enumerates the project's supporting
|
|
25
|
-
infrastructure (3rd-party services from CLAUDE.md's `# Supporting 3rd Party Applications` and
|
|
26
|
-
ENVIRONMENT.md — host, port and optional console URL) into an `## Infrastructure` section so
|
|
27
|
-
the generated CI/CD app can surface a status-only Infrastructure page. The plan carries a
|
|
28
|
-
top-level
|
|
29
|
-
`**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
|
|
30
|
-
it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
|
|
31
|
-
after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
|
|
32
|
-
cicd", "plan deployment", "cicd plan", "infer deployment", "deployment plan", "plan ci/cd".
|
|
33
|
-
Accepts one MANDATORY argument: the project-relative path of the plan-request markdown the
|
|
34
|
-
studio generated (e.g., /util-plancicd execution/cicd-plan-3-1753000000000.md).
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
# Util Plan CI/CD
|
|
38
|
-
|
|
39
|
-
## Inputs
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
/util-plancicd <path-to-plan-request-md>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
| Argument | Required | Description |
|
|
46
|
-
|---|---|---|
|
|
47
|
-
| path | yes | Project-relative path of the studio-generated plan-request markdown under `execution/` |
|
|
48
|
-
|
|
49
|
-
If the argument is missing or the file does not exist, STOP and tell the user to click
|
|
50
|
-
**Plan deployment** in the studio's CI/CD screen first.
|
|
51
|
-
|
|
52
|
-
### Auto-Resolved Paths
|
|
53
|
-
|
|
54
|
-
| Logical file | Path |
|
|
55
|
-
|---|---|
|
|
56
|
-
| Plan request | `<argument>` |
|
|
57
|
-
| Environment | `ENVIRONMENT.md` (project root) |
|
|
58
|
-
| Dev tools | `DEVTOOL.md` (project root) |
|
|
59
|
-
| Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
|
|
60
|
-
| App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
|
|
61
|
-
| App config files | `<app_folder>/.env`, `<app_folder>/.env-*` (and stack-conventional config files) |
|
|
62
|
-
| Infrastructure | `CLAUDE.md` `# Supporting 3rd Party Applications` + `ENVIRONMENT.md` |
|
|
63
|
-
| Output plan | `cicd/CICD_PLAN.md` |
|
|
64
|
-
|
|
65
|
-
## Phase 1 — Gather
|
|
66
|
-
|
|
67
|
-
1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
|
|
68
|
-
2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
|
|
69
|
-
(installed tools + paths: node/pnpm/docker/kubectl/python versions).
|
|
70
|
-
2a. **Docker facts**: record whether Docker is installed (DEVTOOL.md) AND whether
|
|
71
|
-
ENVIRONMENT.md names a **docker repo/registry** — a registry URL or namespace the images
|
|
72
|
-
are tagged for (e.g. `registry.example.com/team`, a Docker Hub org, an ECR/GCR path).
|
|
73
|
-
The docker-image build path (below) activates ONLY when BOTH are present; Docker without
|
|
74
|
-
a configured repo keeps today's behavior (no image builds).
|
|
75
|
-
3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on), `# Port Allocation`
|
|
76
|
-
(per-application ports), and `# Supporting 3rd Party Applications` (the infrastructure the
|
|
77
|
-
applications depend on — databases, caches, object storage, SSO, mail, message brokers, etc.).
|
|
78
|
-
4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
|
|
79
|
-
infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
|
|
80
|
-
`composer.json` → laravel).
|
|
81
|
-
4a. For each application, discover its RUNTIME CONFIG files: list any existing `.env` / `.env-*`
|
|
82
|
-
in the application folder AND the stack-conventional config the start command reads (node/
|
|
83
|
-
laravel → `.env`; Spring Boot → `application.properties` / `application-<profile>.yml` or a
|
|
84
|
-
`.env` when the app loads one). Read the content of each existing file (values may be reused).
|
|
85
|
-
Cross-reference ENVIRONMENT.md for the per-application config values (DB/cache/broker hosts and
|
|
86
|
-
ports, external-service URLs, credentials, secrets, the app's own PORT) and the Infrastructure
|
|
87
|
-
list from step 5 for the hosts/ports the app must point at.
|
|
88
|
-
5. Build the **infrastructure list**: one entry per supporting 3rd-party service the project
|
|
89
|
-
declares. For each, resolve `host` (default `127.0.0.1`), `port` and — when it exposes a web
|
|
90
|
-
console/UI — a `url`, cross-referencing ENVIRONMENT.md for the actual host/port/endpoint. A
|
|
91
|
-
service with no host:port to probe (or a purely external SaaS with no local endpoint) is
|
|
92
|
-
omitted. If the project declares no supporting 3rd-party services, the infrastructure list is
|
|
93
|
-
empty and the plan's `## Infrastructure` section is written empty.
|
|
94
|
-
|
|
95
|
-
## Phase 2 — Decide (per application)
|
|
96
|
-
|
|
97
|
-
- **Working directory (CRITICAL)**: the generated CI/CD app executes every command with the
|
|
98
|
-
**application folder** (`<project>/<folder>`) as its working directory — NEVER the project
|
|
99
|
-
root. All paths inside build/start/stop/deploy commands must therefore be relative to the
|
|
100
|
-
application folder (`mvn -q package`, `java -jar target\app.jar`) — never prefixed with the
|
|
101
|
-
application folder itself (`skolafund_middleware\target\...` is WRONG).
|
|
102
|
-
- **Method**: `container` only when Docker appears in DEVTOOL.md AND the stack has a sensible
|
|
103
|
-
container path; otherwise `process` (bare local process — the default for a dev VM).
|
|
104
|
-
- **Build command**: from the stack (e.g. `pnpm install && pnpm build`, `mvn -q package`,
|
|
105
|
-
`composer install`). Use tool paths from DEVTOOL.md when the tool is not on PATH.
|
|
106
|
-
- **Start command**: the stack's production-ish local start (e.g. `node dist/index.js`,
|
|
107
|
-
`java -jar target/<artifact>.jar`, `php artisan serve --port <port>`); `docker build`/
|
|
108
|
-
`docker run` pair when method=container. For jar-based stacks, resolve the EXACT artifact
|
|
109
|
-
filename — read the pom/gradle build config: a Maven `<finalName>` means
|
|
110
|
-
`target/<finalName>.jar` with NO version suffix (Spring Boot repackage keeps that name and
|
|
111
|
-
leaves a `.jar.original` beside it — never reference the `.original`). Never guess
|
|
112
|
-
`<app>-<version>.jar` and never use a `target/*.jar` glob (shell-dependent, breaks on
|
|
113
|
-
Windows cmd).
|
|
114
|
-
- **Stop**: `process-kill` for bare processes; `docker stop <name>` for containers.
|
|
115
|
-
- **Deploy** (method + command): only when ENVIRONMENT.md/DEVTOOL.md show a deployment target —
|
|
116
|
-
`kubernetes` (`kubectl apply -f <app>/k8s/` when kubectl + a cluster/kubeconfig appear),
|
|
117
|
-
`docker` (`docker run`/`docker compose up -d` when the image is the deliverable),
|
|
118
|
-
`web-server` (copy the built artifact into the configured web/app server, e.g. nginx html
|
|
119
|
-
root or Tomcat webapps, when one is configured). Default **`none`** — a bare local process
|
|
120
|
-
needs no deploy step beyond Start; never invent a target the config does not name.
|
|
121
|
-
- **Docker image** (only when Gather 2a found Docker + a docker repo — otherwise skip this
|
|
122
|
-
bullet entirely):
|
|
123
|
-
- **Image name**: `<repo>/<app_folder>` — the application folder name, lowercased, with
|
|
124
|
-
any character docker rejects replaced by `-` (keep a leading `<number>_` prefix as-is;
|
|
125
|
-
e.g. `registry.example.com/team/skolafund_web`).
|
|
126
|
-
- **Build**: append the image build to the app build —
|
|
127
|
-
`<app build> && docker build -t <image>:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Build never pushes — pushing belongs to a `docker` Deploy method.
|
|
131
|
-
- **Container start/deploy**: when method=container, reference `<image>:
|
|
132
|
-
start/deploy commands too, so the
|
|
133
|
-
- **Dockerfile**: if the application folder already has a `Dockerfile`, use it as-is
|
|
134
|
-
(`existing`). Otherwise author a stack-appropriate Dockerfile (node / spring-boot /
|
|
135
|
-
laravel — same stack detection as depgen-k8s) into the plan's `## Dockerfiles` section
|
|
136
|
-
(`managed`); the generated CI/CD app materializes it before Build only when the file is
|
|
137
|
-
still missing on disk.
|
|
138
|
-
- **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
|
|
139
|
-
collisions with the studio (3001) and the CI/CD app port.
|
|
140
|
-
- **Health / running-detection**: the endpoint the generated CI/CD app uses to tell whether the
|
|
141
|
-
application is already running. Detection is a **raw TCP connect to the app's port on loopback**
|
|
142
|
-
— the generated app probes BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback, plus the host
|
|
143
|
-
named in this Health value — so it does not matter which family the framework binds. This
|
|
144
|
-
matters because dev servers differ: Spring Boot binds `127.0.0.1`, but **Vite / Next preview
|
|
145
|
-
servers bind IPv6 `::1` (localhost) only** and are invisible to an IPv4-only probe. Therefore
|
|
146
|
-
set the Health host to **`localhost`** (not `127.0.0.1`) for any Node/Vite/Next dev-server app
|
|
147
|
-
so the detection host set includes the interface it actually binds; use `127.0.0.1` for JVM
|
|
148
|
-
apps. Value form: `http://localhost:<port>/` or a documented health path from the spec (the
|
|
149
|
-
path is informational — detection keys on the port, not the path).
|
|
150
|
-
- **CI/CD app port**: 5000 unless taken in `# Port Allocation`; record it in the plan.
|
|
151
|
-
|
|
152
|
-
### Application config (per application)
|
|
153
|
-
|
|
154
|
-
Decide the runtime config the application needs so it STARTS SUCCESSFULLY, and where each value
|
|
155
|
-
comes from. For every config file the start command reads (`path`, relative to the application
|
|
156
|
-
folder):
|
|
157
|
-
|
|
158
|
-
- **source** — classify each file:
|
|
159
|
-
- `env-file` — an existing `.env` / `.env-*` in the app folder already holds the runtime values;
|
|
160
|
-
use it as-is on disk (do NOT re-materialize it). Set `managed: no`, no resolved content.
|
|
161
|
-
- `environment` — there is no complete runtime file; resolve the required values from
|
|
162
|
-
ENVIRONMENT.md (and the Infrastructure hosts/ports) and record the fully-resolved file content.
|
|
163
|
-
Set `managed: yes` so the CI/CD app writes it before Build/Start when missing.
|
|
164
|
-
- `derived` — the runtime file is chosen/combined from existing material (e.g. copy the target
|
|
165
|
-
env's `.env-<env>` to the canonical `.env` the start command reads, or merge an existing
|
|
166
|
-
`.env-*` with ENVIRONMENT.md values). Set `managed: yes` and record the resolved content.
|
|
167
|
-
- **required keys** — enumerate every key the app must have to boot: its own PORT, datastore/cache/
|
|
168
|
-
broker connection strings, external-service URLs, credentials/secrets. Cross-check them against
|
|
169
|
-
ENVIRONMENT.md and the Infrastructure list.
|
|
170
|
-
- **completeness (the point of this)** — every required key MUST be satisfiable from ENVIRONMENT.md
|
|
171
|
-
or an existing env file. If a required key cannot be resolved, still emit the config entry but
|
|
172
|
-
call out the unresolved key in that application's Rationale as a GAP (so the human fixes
|
|
173
|
-
ENVIRONMENT.md before approving) — never silently drop it.
|
|
174
|
-
- **managed content** — for a `managed: yes` file, write the resolved body verbatim into the plan
|
|
175
|
-
as a fenced block (`path=<file>`). Values (including credentials) are emitted verbatim under the
|
|
176
|
-
dev-environment plaintext rule; they must never appear in the Rationale narrative.
|
|
177
|
-
|
|
178
|
-
Only list files the app actually reads at runtime — do not invent config files. An app that needs
|
|
179
|
-
no config file gets an empty config list.
|
|
180
|
-
|
|
181
|
-
### Infrastructure (status-only)
|
|
182
|
-
|
|
183
|
-
Supporting 3rd-party services are **not** built/started/deployed by the CI/CD app — they are
|
|
184
|
-
surfaced status-only. For each service resolved in Gather step 5, decide:
|
|
185
|
-
|
|
186
|
-
- **id**: a stable kebab-case key unique within the plan (e.g. `postgres-main`, `keycloak`).
|
|
187
|
-
- **name**: display name (e.g. `PostgreSQL (skf_main)`, `Keycloak`).
|
|
188
|
-
- **host** / **port**: the address the CI/CD app TCP-probes for reachability; default host
|
|
189
|
-
`127.0.0.1`. Use the values ENVIRONMENT.md documents.
|
|
190
|
-
- **url**: the web console/UI opened in a new tab, when the service has one (Keycloak admin,
|
|
191
|
-
MinIO console, Mailcatcher inbox); `-` for headless services (PostgreSQL, Redis).
|
|
192
|
-
- **description**: a short blurb (e.g. `Shared app database`, `S3-compatible object storage`).
|
|
193
|
-
|
|
194
|
-
Never emit credentials into the infrastructure entries — status and links only.
|
|
195
|
-
|
|
196
|
-
## Phase 3 — Write the plan
|
|
197
|
-
|
|
198
|
-
Create `cicd/` if missing and write `cicd/CICD_PLAN.md` (shown here in a 4-backtick fence so the
|
|
199
|
-
inner ```env config block renders — the plan file itself uses a normal 3-backtick env fence):
|
|
200
|
-
|
|
201
|
-
````markdown
|
|
202
|
-
# CI/CD Deployment Plan — <Project Name>
|
|
203
|
-
|
|
204
|
-
**Generated**: <ISO date>
|
|
205
|
-
**Status**: IN PROGRESS
|
|
206
|
-
|
|
207
|
-
## CI/CD App
|
|
208
|
-
|
|
209
|
-
- **Port**: <port>
|
|
210
|
-
- **Folder**: cicd/app
|
|
211
|
-
|
|
212
|
-
## Deployment Decisions
|
|
213
|
-
|
|
214
|
-
| Application | Folder | Method | Port | Build | Start | Stop | Deploy Method | Deploy | Health |
|
|
215
|
-
|---|---|---|---|---|---|---|---|---|---|
|
|
216
|
-
| <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
|
|
217
|
-
|
|
218
|
-
> **Running-detection**: the generated CI/CD app determines "is it running?" by a TCP connect to
|
|
219
|
-
> each application's **Port** on both IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the
|
|
220
|
-
> **Health** host — so an app is detected whichever family it binds. Use a `localhost` Health host
|
|
221
|
-
> for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
##
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
- Never write the CI/CD app; never modify PRD.md/CLAUDE.md/BUG.md.
|
|
307
|
-
- Never delete an existing `**Approved**:` line if re-planning over a previous plan — overwrite
|
|
308
|
-
the whole file WITHOUT any Approved line (a re-plan always requires fresh approval).
|
|
309
|
-
- Credentials from ENVIRONMENT.md may be referenced in commands verbatim (dev-environment
|
|
310
|
-
plaintext rule) but never echoed into the Rationale narrative.
|
|
1
|
+
---
|
|
2
|
+
name: util-plancicd
|
|
3
|
+
model: claude-opus-4-8
|
|
4
|
+
effort: high
|
|
5
|
+
description: >
|
|
6
|
+
Infer a local-deployment plan for every custom application of a CO2 project and write it to
|
|
7
|
+
cicd/CICD_PLAN.md for human review in Compound Context Studio. Reads the project's
|
|
8
|
+
ENVIRONMENT.md and DEVTOOL.md, CLAUDE.md (# Custom Applications, # Port Allocation) and each
|
|
9
|
+
application's context/specification/SPECIFICATION.md, then decides per application: deployment
|
|
10
|
+
method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
|
|
11
|
+
start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
|
|
12
|
+
kubectl apply — only when the environment calls for one), port and health-check URL, plus a
|
|
13
|
+
port for the generated CI/CD app itself. When Docker AND a docker repo/registry are both
|
|
14
|
+
configured (DEVTOOL.md + ENVIRONMENT.md), it additionally plans the docker-image build path:
|
|
15
|
+
per-application image names under the repo, Build commands extended with
|
|
16
|
+
`docker build -t <image>:latest` (no version token — the generated CI/CD app runs commands
|
|
17
|
+
verbatim on the current working tree), and a managed Dockerfile authored into the plan's
|
|
18
|
+
`## Dockerfiles` section for any dockerized application lacking one (materialized before
|
|
19
|
+
Build, never overwriting an existing file). It also infers each application's RUNTIME CONFIG — the
|
|
20
|
+
config files its start command reads (e.g. `.env`), deciding per file whether the values come
|
|
21
|
+
from ENVIRONMENT.md or from an existing `.env` / `.env-*` file already in the app folder, listing
|
|
22
|
+
the required keys and, for ENVIRONMENT.md-sourced files, the resolved content — so the generated
|
|
23
|
+
CI/CD app can complete the config before start (guaranteeing the app comes up) and show it in a
|
|
24
|
+
per-application Config modal for troubleshooting. It also enumerates the project's supporting
|
|
25
|
+
infrastructure (3rd-party services from CLAUDE.md's `# Supporting 3rd Party Applications` and
|
|
26
|
+
ENVIRONMENT.md — host, port and optional console URL) into an `## Infrastructure` section so
|
|
27
|
+
the generated CI/CD app can surface a status-only Infrastructure page. The plan carries a
|
|
28
|
+
top-level
|
|
29
|
+
`**Status**: IN PROGRESS` header flipped to `COMPLETED` when done — the studio's watcher reads
|
|
30
|
+
it. This skill NEVER generates the CI/CD app (that is util-gencicdscript, which runs only
|
|
31
|
+
after the studio stamps an **Approved**: line into the plan). Trigger on keywords: "plan
|
|
32
|
+
cicd", "plan deployment", "cicd plan", "infer deployment", "deployment plan", "plan ci/cd".
|
|
33
|
+
Accepts one MANDATORY argument: the project-relative path of the plan-request markdown the
|
|
34
|
+
studio generated (e.g., /util-plancicd execution/cicd-plan-3-1753000000000.md).
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Util Plan CI/CD
|
|
38
|
+
|
|
39
|
+
## Inputs
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
/util-plancicd <path-to-plan-request-md>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Argument | Required | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| path | yes | Project-relative path of the studio-generated plan-request markdown under `execution/` |
|
|
48
|
+
|
|
49
|
+
If the argument is missing or the file does not exist, STOP and tell the user to click
|
|
50
|
+
**Plan deployment** in the studio's CI/CD screen first.
|
|
51
|
+
|
|
52
|
+
### Auto-Resolved Paths
|
|
53
|
+
|
|
54
|
+
| Logical file | Path |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Plan request | `<argument>` |
|
|
57
|
+
| Environment | `ENVIRONMENT.md` (project root) |
|
|
58
|
+
| Dev tools | `DEVTOOL.md` (project root) |
|
|
59
|
+
| Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
|
|
60
|
+
| App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
|
|
61
|
+
| App config files | `<app_folder>/.env`, `<app_folder>/.env-*` (and stack-conventional config files) |
|
|
62
|
+
| Infrastructure | `CLAUDE.md` `# Supporting 3rd Party Applications` + `ENVIRONMENT.md` |
|
|
63
|
+
| Output plan | `cicd/CICD_PLAN.md` |
|
|
64
|
+
|
|
65
|
+
## Phase 1 — Gather
|
|
66
|
+
|
|
67
|
+
1. Read the plan-request markdown; its `## Applications` list is the authoritative application set.
|
|
68
|
+
2. Read `ENVIRONMENT.md` (registries, credentials, external services) and `DEVTOOL.md`
|
|
69
|
+
(installed tools + paths: node/pnpm/docker/kubectl/python versions).
|
|
70
|
+
2a. **Docker facts**: record whether Docker is installed (DEVTOOL.md) AND whether
|
|
71
|
+
ENVIRONMENT.md names a **docker repo/registry** — a registry URL or namespace the images
|
|
72
|
+
are tagged for (e.g. `registry.example.com/team`, a Docker Hub org, an ECR/GCR path).
|
|
73
|
+
The docker-image build path (below) activates ONLY when BOTH are present; Docker without
|
|
74
|
+
a configured repo keeps today's behavior (no image builds).
|
|
75
|
+
3. Read `CLAUDE.md`: `# Custom Applications` (folder names, Depends on), `# Port Allocation`
|
|
76
|
+
(per-application ports), and `# Supporting 3rd Party Applications` (the infrastructure the
|
|
77
|
+
applications depend on — databases, caches, object storage, SSO, mail, message brokers, etc.).
|
|
78
|
+
4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
|
|
79
|
+
infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
|
|
80
|
+
`composer.json` → laravel).
|
|
81
|
+
4a. For each application, discover its RUNTIME CONFIG files: list any existing `.env` / `.env-*`
|
|
82
|
+
in the application folder AND the stack-conventional config the start command reads (node/
|
|
83
|
+
laravel → `.env`; Spring Boot → `application.properties` / `application-<profile>.yml` or a
|
|
84
|
+
`.env` when the app loads one). Read the content of each existing file (values may be reused).
|
|
85
|
+
Cross-reference ENVIRONMENT.md for the per-application config values (DB/cache/broker hosts and
|
|
86
|
+
ports, external-service URLs, credentials, secrets, the app's own PORT) and the Infrastructure
|
|
87
|
+
list from step 5 for the hosts/ports the app must point at.
|
|
88
|
+
5. Build the **infrastructure list**: one entry per supporting 3rd-party service the project
|
|
89
|
+
declares. For each, resolve `host` (default `127.0.0.1`), `port` and — when it exposes a web
|
|
90
|
+
console/UI — a `url`, cross-referencing ENVIRONMENT.md for the actual host/port/endpoint. A
|
|
91
|
+
service with no host:port to probe (or a purely external SaaS with no local endpoint) is
|
|
92
|
+
omitted. If the project declares no supporting 3rd-party services, the infrastructure list is
|
|
93
|
+
empty and the plan's `## Infrastructure` section is written empty.
|
|
94
|
+
|
|
95
|
+
## Phase 2 — Decide (per application)
|
|
96
|
+
|
|
97
|
+
- **Working directory (CRITICAL)**: the generated CI/CD app executes every command with the
|
|
98
|
+
**application folder** (`<project>/<folder>`) as its working directory — NEVER the project
|
|
99
|
+
root. All paths inside build/start/stop/deploy commands must therefore be relative to the
|
|
100
|
+
application folder (`mvn -q package`, `java -jar target\app.jar`) — never prefixed with the
|
|
101
|
+
application folder itself (`skolafund_middleware\target\...` is WRONG).
|
|
102
|
+
- **Method**: `container` only when Docker appears in DEVTOOL.md AND the stack has a sensible
|
|
103
|
+
container path; otherwise `process` (bare local process — the default for a dev VM).
|
|
104
|
+
- **Build command**: from the stack (e.g. `pnpm install && pnpm build`, `mvn -q package`,
|
|
105
|
+
`composer install`). Use tool paths from DEVTOOL.md when the tool is not on PATH.
|
|
106
|
+
- **Start command**: the stack's production-ish local start (e.g. `node dist/index.js`,
|
|
107
|
+
`java -jar target/<artifact>.jar`, `php artisan serve --port <port>`); `docker build`/
|
|
108
|
+
`docker run` pair when method=container. For jar-based stacks, resolve the EXACT artifact
|
|
109
|
+
filename — read the pom/gradle build config: a Maven `<finalName>` means
|
|
110
|
+
`target/<finalName>.jar` with NO version suffix (Spring Boot repackage keeps that name and
|
|
111
|
+
leaves a `.jar.original` beside it — never reference the `.original`). Never guess
|
|
112
|
+
`<app>-<version>.jar` and never use a `target/*.jar` glob (shell-dependent, breaks on
|
|
113
|
+
Windows cmd).
|
|
114
|
+
- **Stop**: `process-kill` for bare processes; `docker stop <name>` for containers.
|
|
115
|
+
- **Deploy** (method + command): only when ENVIRONMENT.md/DEVTOOL.md show a deployment target —
|
|
116
|
+
`kubernetes` (`kubectl apply -f <app>/k8s/` when kubectl + a cluster/kubeconfig appear),
|
|
117
|
+
`docker` (`docker run`/`docker compose up -d` when the image is the deliverable),
|
|
118
|
+
`web-server` (copy the built artifact into the configured web/app server, e.g. nginx html
|
|
119
|
+
root or Tomcat webapps, when one is configured). Default **`none`** — a bare local process
|
|
120
|
+
needs no deploy step beyond Start; never invent a target the config does not name.
|
|
121
|
+
- **Docker image** (only when Gather 2a found Docker + a docker repo — otherwise skip this
|
|
122
|
+
bullet entirely):
|
|
123
|
+
- **Image name**: `<repo>/<app_folder>` — the application folder name, lowercased, with
|
|
124
|
+
any character docker rejects replaced by `-` (keep a leading `<number>_` prefix as-is;
|
|
125
|
+
e.g. `registry.example.com/team/skolafund_web`).
|
|
126
|
+
- **Build**: append the image build to the app build —
|
|
127
|
+
`<app build> && docker build -t <image>:latest .` Commands carry NO `{VERSION}` token or
|
|
128
|
+
any other placeholder — the generated CI/CD app runs them verbatim; only the current
|
|
129
|
+
working tree is ever built and images are always tagged `latest`.
|
|
130
|
+
Build never pushes — pushing belongs to a `docker` Deploy method.
|
|
131
|
+
- **Container start/deploy**: when method=container, reference `<image>:latest` in the
|
|
132
|
+
start/deploy commands too, so the freshly built tag is what runs.
|
|
133
|
+
- **Dockerfile**: if the application folder already has a `Dockerfile`, use it as-is
|
|
134
|
+
(`existing`). Otherwise author a stack-appropriate Dockerfile (node / spring-boot /
|
|
135
|
+
laravel — same stack detection as depgen-k8s) into the plan's `## Dockerfiles` section
|
|
136
|
+
(`managed`); the generated CI/CD app materializes it before Build only when the file is
|
|
137
|
+
still missing on disk.
|
|
138
|
+
- **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
|
|
139
|
+
collisions with the studio (3001) and the CI/CD app port.
|
|
140
|
+
- **Health / running-detection**: the endpoint the generated CI/CD app uses to tell whether the
|
|
141
|
+
application is already running. Detection is a **raw TCP connect to the app's port on loopback**
|
|
142
|
+
— the generated app probes BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback, plus the host
|
|
143
|
+
named in this Health value — so it does not matter which family the framework binds. This
|
|
144
|
+
matters because dev servers differ: Spring Boot binds `127.0.0.1`, but **Vite / Next preview
|
|
145
|
+
servers bind IPv6 `::1` (localhost) only** and are invisible to an IPv4-only probe. Therefore
|
|
146
|
+
set the Health host to **`localhost`** (not `127.0.0.1`) for any Node/Vite/Next dev-server app
|
|
147
|
+
so the detection host set includes the interface it actually binds; use `127.0.0.1` for JVM
|
|
148
|
+
apps. Value form: `http://localhost:<port>/` or a documented health path from the spec (the
|
|
149
|
+
path is informational — detection keys on the port, not the path).
|
|
150
|
+
- **CI/CD app port**: 5000 unless taken in `# Port Allocation`; record it in the plan.
|
|
151
|
+
|
|
152
|
+
### Application config (per application)
|
|
153
|
+
|
|
154
|
+
Decide the runtime config the application needs so it STARTS SUCCESSFULLY, and where each value
|
|
155
|
+
comes from. For every config file the start command reads (`path`, relative to the application
|
|
156
|
+
folder):
|
|
157
|
+
|
|
158
|
+
- **source** — classify each file:
|
|
159
|
+
- `env-file` — an existing `.env` / `.env-*` in the app folder already holds the runtime values;
|
|
160
|
+
use it as-is on disk (do NOT re-materialize it). Set `managed: no`, no resolved content.
|
|
161
|
+
- `environment` — there is no complete runtime file; resolve the required values from
|
|
162
|
+
ENVIRONMENT.md (and the Infrastructure hosts/ports) and record the fully-resolved file content.
|
|
163
|
+
Set `managed: yes` so the CI/CD app writes it before Build/Start when missing.
|
|
164
|
+
- `derived` — the runtime file is chosen/combined from existing material (e.g. copy the target
|
|
165
|
+
env's `.env-<env>` to the canonical `.env` the start command reads, or merge an existing
|
|
166
|
+
`.env-*` with ENVIRONMENT.md values). Set `managed: yes` and record the resolved content.
|
|
167
|
+
- **required keys** — enumerate every key the app must have to boot: its own PORT, datastore/cache/
|
|
168
|
+
broker connection strings, external-service URLs, credentials/secrets. Cross-check them against
|
|
169
|
+
ENVIRONMENT.md and the Infrastructure list.
|
|
170
|
+
- **completeness (the point of this)** — every required key MUST be satisfiable from ENVIRONMENT.md
|
|
171
|
+
or an existing env file. If a required key cannot be resolved, still emit the config entry but
|
|
172
|
+
call out the unresolved key in that application's Rationale as a GAP (so the human fixes
|
|
173
|
+
ENVIRONMENT.md before approving) — never silently drop it.
|
|
174
|
+
- **managed content** — for a `managed: yes` file, write the resolved body verbatim into the plan
|
|
175
|
+
as a fenced block (`path=<file>`). Values (including credentials) are emitted verbatim under the
|
|
176
|
+
dev-environment plaintext rule; they must never appear in the Rationale narrative.
|
|
177
|
+
|
|
178
|
+
Only list files the app actually reads at runtime — do not invent config files. An app that needs
|
|
179
|
+
no config file gets an empty config list.
|
|
180
|
+
|
|
181
|
+
### Infrastructure (status-only)
|
|
182
|
+
|
|
183
|
+
Supporting 3rd-party services are **not** built/started/deployed by the CI/CD app — they are
|
|
184
|
+
surfaced status-only. For each service resolved in Gather step 5, decide:
|
|
185
|
+
|
|
186
|
+
- **id**: a stable kebab-case key unique within the plan (e.g. `postgres-main`, `keycloak`).
|
|
187
|
+
- **name**: display name (e.g. `PostgreSQL (skf_main)`, `Keycloak`).
|
|
188
|
+
- **host** / **port**: the address the CI/CD app TCP-probes for reachability; default host
|
|
189
|
+
`127.0.0.1`. Use the values ENVIRONMENT.md documents.
|
|
190
|
+
- **url**: the web console/UI opened in a new tab, when the service has one (Keycloak admin,
|
|
191
|
+
MinIO console, Mailcatcher inbox); `-` for headless services (PostgreSQL, Redis).
|
|
192
|
+
- **description**: a short blurb (e.g. `Shared app database`, `S3-compatible object storage`).
|
|
193
|
+
|
|
194
|
+
Never emit credentials into the infrastructure entries — status and links only.
|
|
195
|
+
|
|
196
|
+
## Phase 3 — Write the plan
|
|
197
|
+
|
|
198
|
+
Create `cicd/` if missing and write `cicd/CICD_PLAN.md` (shown here in a 4-backtick fence so the
|
|
199
|
+
inner ```env config block renders — the plan file itself uses a normal 3-backtick env fence):
|
|
200
|
+
|
|
201
|
+
````markdown
|
|
202
|
+
# CI/CD Deployment Plan — <Project Name>
|
|
203
|
+
|
|
204
|
+
**Generated**: <ISO date>
|
|
205
|
+
**Status**: IN PROGRESS
|
|
206
|
+
|
|
207
|
+
## CI/CD App
|
|
208
|
+
|
|
209
|
+
- **Port**: <port>
|
|
210
|
+
- **Folder**: cicd/app
|
|
211
|
+
|
|
212
|
+
## Deployment Decisions
|
|
213
|
+
|
|
214
|
+
| Application | Folder | Method | Port | Build | Start | Stop | Deploy Method | Deploy | Health |
|
|
215
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
216
|
+
| <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
|
|
217
|
+
|
|
218
|
+
> **Running-detection**: the generated CI/CD app determines "is it running?" by a TCP connect to
|
|
219
|
+
> each application's **Port** on both IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the
|
|
220
|
+
> **Health** host — so an app is detected whichever family it binds. Use a `localhost` Health host
|
|
221
|
+
> for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
|
|
222
|
+
|
|
223
|
+
## Docker
|
|
224
|
+
|
|
225
|
+
Written only from Gather 2a. When Docker or a docker repo is absent, write the section as:
|
|
226
|
+
`_Not dockerized — Docker and/or a docker repo are not configured in DEVTOOL.md/ENVIRONMENT.md._`
|
|
227
|
+
|
|
228
|
+
**Registry**: <repo, e.g. `registry.example.com/team`>
|
|
229
|
+
|
|
230
|
+
| Application | Image | Dockerfile |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| <name> | `<repo>/<app_folder>` | existing \| managed |
|
|
233
|
+
|
|
234
|
+
## Dockerfiles
|
|
235
|
+
|
|
236
|
+
One fenced block per `managed` Dockerfile (omit the section body — `_All Dockerfiles exist._`
|
|
237
|
+
— when every dockerized app already has one; `_Not dockerized._` when the Docker section is
|
|
238
|
+
inactive). The generated CI/CD app writes each body to `<app_folder>/Dockerfile` before Build
|
|
239
|
+
only when the file is missing on disk — an existing Dockerfile is never overwritten.
|
|
240
|
+
|
|
241
|
+
```dockerfile path=<app_folder>/Dockerfile
|
|
242
|
+
FROM node:22-slim
|
|
243
|
+
WORKDIR /app
|
|
244
|
+
COPY package*.json ./
|
|
245
|
+
RUN npm ci --omit=dev
|
|
246
|
+
COPY . .
|
|
247
|
+
EXPOSE <port>
|
|
248
|
+
CMD ["node", "dist/index.js"]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Application Configs
|
|
252
|
+
|
|
253
|
+
The runtime config each application needs to START SUCCESSFULLY. The generated CI/CD app writes any
|
|
254
|
+
`managed=yes` file into the application folder before Build/Start when it is missing, and shows
|
|
255
|
+
every file (source, status, required keys, live content) in the per-application Config modal for
|
|
256
|
+
troubleshooting. One table per application; `Path` is relative to the application folder.
|
|
257
|
+
|
|
258
|
+
### <App Name> (<folder>)
|
|
259
|
+
|
|
260
|
+
| Path | Source | Managed | Required Keys |
|
|
261
|
+
|---|---|---|---|
|
|
262
|
+
| .env | environment | yes | DATABASE_URL, REDIS_HOST, PORT |
|
|
263
|
+
| .env-uat | env-file | no | DATABASE_URL, REDIS_HOST |
|
|
264
|
+
|
|
265
|
+
For each `Managed=yes` row, follow it with the resolved file body (verbatim — credentials allowed
|
|
266
|
+
under the dev-plaintext rule):
|
|
267
|
+
|
|
268
|
+
```env path=.env
|
|
269
|
+
DATABASE_URL=postgres://app:app@127.0.0.1:5432/appdb
|
|
270
|
+
REDIS_HOST=127.0.0.1
|
|
271
|
+
PORT=3000
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Omit the fenced block for `env-file` rows (the file is used as-is on disk). If an application needs
|
|
275
|
+
no config file, write `_No runtime config files._` under its heading.
|
|
276
|
+
|
|
277
|
+
## Infrastructure
|
|
278
|
+
|
|
279
|
+
Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
|
|
280
|
+
Infrastructure page. Leave the table body empty when the project declares no supporting services.
|
|
281
|
+
|
|
282
|
+
| Id | Service | Host | Port | Console URL | Description |
|
|
283
|
+
|---|---|---|---|---|---|
|
|
284
|
+
| <id> | <name> | <host> | <port> | `<url or ->` | <blurb> |
|
|
285
|
+
|
|
286
|
+
## Rationale
|
|
287
|
+
|
|
288
|
+
- <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
|
|
289
|
+
````
|
|
290
|
+
|
|
291
|
+
Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
|
|
292
|
+
only on COMPLETED.
|
|
293
|
+
|
|
294
|
+
## STOP — Hand Back to the Human
|
|
295
|
+
|
|
296
|
+
Print a one-paragraph summary and STOP. Never invoke util-gencicdscript or any other skill —
|
|
297
|
+
the human reviews and approves the plan in the studio's CI/CD screen first.
|
|
298
|
+
|
|
299
|
+
## Important Rules
|
|
300
|
+
|
|
301
|
+
- Never write the CI/CD app; never modify PRD.md/CLAUDE.md/BUG.md.
|
|
302
|
+
- Never delete an existing `**Approved**:` line if re-planning over a previous plan — overwrite
|
|
303
|
+
the whole file WITHOUT any Approved line (a re-plan always requires fresh approval).
|
|
304
|
+
- Credentials from ENVIRONMENT.md may be referenced in commands verbatim (dev-environment
|
|
305
|
+
plaintext rule) but never echoed into the Rationale narrative.
|