@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.
Binary file
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.16",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rashidee/co2",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
4
4
  "description": "Compound Context Studio — self-hosted team context authoring for CO2 projects",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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. The top bar carries a version selector (fed by
25
- the studio guard's versions, latest preselected) whose choice replaces the {VERSION} token in
26
- commands docker images are tagged <repo>/<app>:<version> when the plan's Docker section is
27
- active (managed Dockerfiles materialized before Build when missing; Build never pushes) — and
28
- Build All / Start All / Stop All buttons operating every eligible application at once
29
- (sequential builds). When the approved plan declares an `## Infrastructure` list, the
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 may carry the literal `{VERSION}` tokencopy it verbatim; the generated app
79
- substitutes it at execution. `deployMethod` is the plan's Deploy Method column
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
- - **Version selector (topbar)**: the page's top bar carries a version dropdown fed by the
167
- studio guard response's `versions` array (already polled every 30 s; latest first
168
- preselected). When the studio is unreachable the selector holds a single `latest` entry.
169
- The selected semver, minus its `v` prefix, replaces every literal `{VERSION}` in
170
- build/start/stop/deploy commands at execution time (fallback `latest`). Tag-only
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, version, onDone) {
253
- if (activity.has(app.name)) { onDone && onDone(); return }
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, subst(app.build, version), (code) => {
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, version) {
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, subst(app.deploy, version), (code) => {
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, version) {
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, subst(app.start, version), () => {})
269
+ runLogged(app, app.start, () => {})
281
270
  saveState(); return
282
271
  }
283
- const child = runLogged(app, subst(app.start, version), (code) => {
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, version) {
319
+ function stopApp(app) {
331
320
  if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
332
- const stopCmd = subst(app.stop, version)
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+':<version>':'Runs the plan build command'}">Build</button>
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
- ws = new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+location.pathname.replace(/\/$/,'')+'/ws?app='+encodeURIComponent(name))
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+'?version='+encodeURIComponent(selectedVersion()), { method: 'POST' })
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, reqVersion)
640
- else if (m[2] === 'start') startApp(app, reqVersion)
641
- else if (m[2] === 'deploy') deployApp(app, reqVersion)
642
- else stopApp(app, reqVersion)
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 may carry the literal `{VERSION}` token — copied verbatim; the
722
- server substitutes the topbar-selected version (semver sans `v`, fallback `latest`) at
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:{VERSION} .",
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>:{VERSION}` (the {VERSION} token is resolved by the generated CI/CD
17
- app from its topbar version selector), 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>:{VERSION} .` The literal `{VERSION}` token is
128
- the contract: the generated CI/CD app substitutes the version selected in its topbar
129
- (semver without the `v` prefix; `latest` fallback) into commands at execution time.
130
- Build never pushes — pushing belongs to a `docker` Deploy method.
131
- - **Container start/deploy**: when method=container, reference `<image>:{VERSION}` in the
132
- start/deploy commands too, so the selected 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
- > **`{VERSION}` token**: Build/Start/Deploy commands may contain the literal `{VERSION}`;
224
- > the generated CI/CD app replaces it with the version selected in its topbar (semver
225
- > without the `v` prefix, `latest` when unknown) at execution time. Used by the docker-image
226
- > path to tag images; tag-only the code built is always the working tree.
227
-
228
- ## Docker
229
-
230
- Written only from Gather 2a. When Docker or a docker repo is absent, write the section as:
231
- `_Not dockerized — Docker and/or a docker repo are not configured in DEVTOOL.md/ENVIRONMENT.md._`
232
-
233
- **Registry**: <repo, e.g. `registry.example.com/team`>
234
-
235
- | Application | Image | Dockerfile |
236
- |---|---|---|
237
- | <name> | `<repo>/<app_folder>` | existing \| managed |
238
-
239
- ## Dockerfiles
240
-
241
- One fenced block per `managed` Dockerfile (omit the section body — `_All Dockerfiles exist._`
242
- when every dockerized app already has one; `_Not dockerized._` when the Docker section is
243
- inactive). The generated CI/CD app writes each body to `<app_folder>/Dockerfile` before Build
244
- only when the file is missing on disk — an existing Dockerfile is never overwritten.
245
-
246
- ```dockerfile path=<app_folder>/Dockerfile
247
- FROM node:22-slim
248
- WORKDIR /app
249
- COPY package*.json ./
250
- RUN npm ci --omit=dev
251
- COPY . .
252
- EXPOSE <port>
253
- CMD ["node", "dist/index.js"]
254
- ```
255
-
256
- ## Application Configs
257
-
258
- The runtime config each application needs to START SUCCESSFULLY. The generated CI/CD app writes any
259
- `managed=yes` file into the application folder before Build/Start when it is missing, and shows
260
- every file (source, status, required keys, live content) in the per-application Config modal for
261
- troubleshooting. One table per application; `Path` is relative to the application folder.
262
-
263
- ### <App Name> (<folder>)
264
-
265
- | Path | Source | Managed | Required Keys |
266
- |---|---|---|---|
267
- | .env | environment | yes | DATABASE_URL, REDIS_HOST, PORT |
268
- | .env-uat | env-file | no | DATABASE_URL, REDIS_HOST |
269
-
270
- For each `Managed=yes` row, follow it with the resolved file body (verbatim — credentials allowed
271
- under the dev-plaintext rule):
272
-
273
- ```env path=.env
274
- DATABASE_URL=postgres://app:app@127.0.0.1:5432/appdb
275
- REDIS_HOST=127.0.0.1
276
- PORT=3000
277
- ```
278
-
279
- Omit the fenced block for `env-file` rows (the file is used as-is on disk). If an application needs
280
- no config file, write `_No runtime config files._` under its heading.
281
-
282
- ## Infrastructure
283
-
284
- Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
285
- Infrastructure page. Leave the table body empty when the project declares no supporting services.
286
-
287
- | Id | Service | Host | Port | Console URL | Description |
288
- |---|---|---|---|---|---|
289
- | <id> | <name> | <host> | <port> | `<url or ->` | <blurb> |
290
-
291
- ## Rationale
292
-
293
- - <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
294
- ````
295
-
296
- Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
297
- only on COMPLETED.
298
-
299
- ## STOP — Hand Back to the Human
300
-
301
- Print a one-paragraph summary and STOP. Never invoke util-gencicdscript or any other skill
302
- the human reviews and approves the plan in the studio's CI/CD screen first.
303
-
304
- ## Important Rules
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.