@rashidee/co2 1.3.4 → 1.3.6
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 +0 -0
- package/dist/.co2-dat/app.db-shm +0 -0
- package/dist/.co2-dat/app.db-wal +0 -0
- package/dist/index.js +916 -321
- package/drizzle/0017_git_remote_url.sql +3 -0
- package/drizzle/0018_default_branch.sql +3 -0
- package/drizzle/0019_git_provider.sql +3 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +1 -1
- package/plugin/skills/util-gencicdscript/SKILL.md +41 -9
- package/plugin/skills/util-gencicdscript/references/cicd-app-template.md +210 -21
- package/plugin/skills/util-plancicd/SKILL.md +88 -5
- package/static/assets/{abnfDiagram-VRR7QNED-BwxjNDj8.js → abnfDiagram-VRR7QNED-CzKpxD9q.js} +1 -1
- package/static/assets/{arc-Bf00Zylw.js → arc-0jj0p8Qe.js} +1 -1
- package/static/assets/{architectureDiagram-ZJ3FMSHR-CRUg8Wt3.js → architectureDiagram-ZJ3FMSHR-DXUL6S5t.js} +1 -1
- package/static/assets/{blockDiagram-677ZJIJ3-Dy-JP_0N.js → blockDiagram-677ZJIJ3-BBjsQESd.js} +1 -1
- package/static/assets/{c4Diagram-LMCZKHZV-obM4PBxI.js → c4Diagram-LMCZKHZV-CTsLC95S.js} +1 -1
- package/static/assets/channel-1MKDXK8w.js +1 -0
- package/static/assets/{chunk-2Q5K7J3B-DoXzquQ1.js → chunk-2Q5K7J3B-BpisBKYx.js} +1 -1
- package/static/assets/{chunk-32BRIVSS-B6VgB7K8.js → chunk-32BRIVSS-DAfsr4ph.js} +1 -1
- package/static/assets/{chunk-5VM5RSS4-BmZS0Ftn.js → chunk-5VM5RSS4-Z3FtY-B6.js} +1 -1
- package/static/assets/{chunk-EX3LRPZG-DdOLrDUP.js → chunk-EX3LRPZG-FI9smQaN.js} +1 -1
- package/static/assets/{chunk-JWPE2WC7-RVdgT8k5.js → chunk-JWPE2WC7-sOygQj_B.js} +1 -1
- package/static/assets/{chunk-MOJQB5TN-BbMHSSXk.js → chunk-MOJQB5TN-DCSpai7w.js} +1 -1
- package/static/assets/{chunk-RYQCIY6F-CBOyejV-.js → chunk-RYQCIY6F-CchqtfnI.js} +1 -1
- package/static/assets/{chunk-V7JOEXUC-DVvl5ZDu.js → chunk-V7JOEXUC-DaOzsfx7.js} +1 -1
- package/static/assets/{chunk-VR4S4FIN-TKs-_Nin.js → chunk-VR4S4FIN-DmxuoVTy.js} +1 -1
- package/static/assets/{chunk-XXDRQBXY-BuwUqVAV.js → chunk-XXDRQBXY-ChERxfri.js} +1 -1
- package/static/assets/classDiagram-OUVF2IWQ-bADaUwoJ.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-bADaUwoJ.js +1 -0
- package/static/assets/{cose-bilkent-JH36ORCC-bVLOVjVk.js → cose-bilkent-JH36ORCC-CO7_eF6b.js} +1 -1
- package/static/assets/{cynefin-VYW2F7L2-B7HPqh-Z.js → cynefin-VYW2F7L2-CzQkWtdY.js} +1 -1
- package/static/assets/{cynefinDiagram-TSTJHNR4-0d2yxXQG.js → cynefinDiagram-TSTJHNR4-BN2o8rFa.js} +1 -1
- package/static/assets/{dagre-VKFMJZFB-D7ErrCay.js → dagre-VKFMJZFB-GLjBE-Xs.js} +1 -1
- package/static/assets/{diagram-FQU43EPY-C36bU8jh.js → diagram-FQU43EPY-Cyv7saju.js} +1 -1
- package/static/assets/{diagram-G47NLZAW-BzaLh10R.js → diagram-G47NLZAW-B9PmZJ3U.js} +1 -1
- package/static/assets/{diagram-NH7WQ7WH-v-Zsf5gF.js → diagram-NH7WQ7WH-ByKD8yJU.js} +1 -1
- package/static/assets/{diagram-OA4YK3LP-CRbJBq_k.js → diagram-OA4YK3LP-BvbW7fIS.js} +1 -1
- package/static/assets/{diagram-WEI45ONY-8fV5Plo4.js → diagram-WEI45ONY-BgXZ9SJ8.js} +1 -1
- package/static/assets/{ebnfDiagram-CCIWWBDH-5gIe63LE.js → ebnfDiagram-CCIWWBDH-CXEEJ26B.js} +1 -1
- package/static/assets/{erDiagram-Q63AITRT-C3IcAHjG.js → erDiagram-Q63AITRT-CcHs_S16.js} +1 -1
- package/static/assets/{flowDiagram-23GEKE2U-KQjlOZK9.js → flowDiagram-23GEKE2U-Cwg3I3GK.js} +1 -1
- package/static/assets/{ganttDiagram-NO4QXBWP-Dq3c2GZf.js → ganttDiagram-NO4QXBWP-ky6w5Lvy.js} +1 -1
- package/static/assets/{gitGraphDiagram-IHSO6WYX-BYtnaU27.js → gitGraphDiagram-IHSO6WYX-DN0eRQ7r.js} +1 -1
- package/static/assets/{index-CgNepcKp.js → index-CCC4-Ibh.js} +174 -172
- package/static/assets/{index-Dn_JY-18.css → index-Cz5Fcq3G.css} +1 -1
- package/static/assets/{infoDiagram-FWYZ7A6U-CxqBig2C.js → infoDiagram-FWYZ7A6U-COojw6OC.js} +1 -1
- package/static/assets/{ishikawaDiagram-FXEZZL3T-0HfzC1wj.js → ishikawaDiagram-FXEZZL3T-BR5w5ZqM.js} +1 -1
- package/static/assets/{journeyDiagram-5HDEW3XC-umkaBeJl.js → journeyDiagram-5HDEW3XC-B5WtTEBM.js} +1 -1
- package/static/assets/{kanban-definition-HUTT4EX6-DsBos3y7.js → kanban-definition-HUTT4EX6-DnQ_zg4E.js} +1 -1
- package/static/assets/{linear-DtNdgaOB.js → linear-C5kGzenz.js} +1 -1
- package/static/assets/{mindmap-definition-LN4V7U3C-BThlwYQu.js → mindmap-definition-LN4V7U3C-BMksxBso.js} +1 -1
- package/static/assets/{pegDiagram-2B236MQR-DoxrNvw3.js → pegDiagram-2B236MQR-BKrF97iU.js} +1 -1
- package/static/assets/{pieDiagram-ENE6RG2P-BqCJUG4Q.js → pieDiagram-ENE6RG2P-DZAQkZwC.js} +1 -1
- package/static/assets/{quadrantDiagram-ABIIQ3AL-4GVp_BSP.js → quadrantDiagram-ABIIQ3AL-Dx0-5odM.js} +1 -1
- package/static/assets/{railroadDiagram-RFXS5EU6-CU341ElP.js → railroadDiagram-RFXS5EU6-BbY-CxL_.js} +1 -1
- package/static/assets/{requirementDiagram-TGXJPOKE-DoBSr-cX.js → requirementDiagram-TGXJPOKE-ItSYgvcN.js} +1 -1
- package/static/assets/{sankeyDiagram-HTMAVEWB-CJ__IqT6.js → sankeyDiagram-HTMAVEWB-CjIuRRYE.js} +1 -1
- package/static/assets/{sequenceDiagram-DBY2YBRQ-BkNwP7Io.js → sequenceDiagram-DBY2YBRQ-B277PT2z.js} +1 -1
- package/static/assets/{sizeCapture-X5ZJPWSS-B-2Xu5n9.js → sizeCapture-X5ZJPWSS-BYc46yWE.js} +1 -1
- package/static/assets/{stateDiagram-2N3HPSRC-DPbpX5_5.js → stateDiagram-2N3HPSRC-Cq41gYoq.js} +1 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-B9V3_HpU.js +1 -0
- package/static/assets/{swimlanes-5IMT3BWC-CnBZNoN8.js → swimlanes-5IMT3BWC-DMxA2jQL.js} +2 -2
- package/static/assets/swimlanesDiagram-G3AALYLV-qx8IX4cU.js +8 -0
- package/static/assets/{timeline-definition-FHXFAJF6-iVUc1pZx.js → timeline-definition-FHXFAJF6-CB8iORNI.js} +1 -1
- package/static/assets/{vennDiagram-L72KCM5P-CL66_DTK.js → vennDiagram-L72KCM5P-CHwAX4cP.js} +1 -1
- package/static/assets/{wardleyDiagram-EHGQE667-DQHcCKUc.js → wardleyDiagram-EHGQE667-DL19kD5s.js} +1 -1
- package/static/assets/{xychartDiagram-FW5EYKEG-B8thIce8.js → xychartDiagram-FW5EYKEG-b271oXxd.js} +1 -1
- package/static/index.html +2 -2
- package/static/assets/channel-BeHa2ubV.js +0 -1
- package/static/assets/classDiagram-OUVF2IWQ-B0BHpjWc.js +0 -1
- package/static/assets/classDiagram-v2-EOCWNBFH-B0BHpjWc.js +0 -1
- package/static/assets/stateDiagram-v2-6OUMAXLB-1Opbis3l.js +0 -1
- package/static/assets/swimlanesDiagram-G3AALYLV-aoTIjn7w.js +0 -8
|
@@ -120,6 +120,27 @@
|
|
|
120
120
|
"when": 1784700000000,
|
|
121
121
|
"tag": "0016_cicd_v2100",
|
|
122
122
|
"breakpoints": true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"idx": 17,
|
|
126
|
+
"version": "6",
|
|
127
|
+
"when": 1784800000000,
|
|
128
|
+
"tag": "0017_git_remote_url",
|
|
129
|
+
"breakpoints": true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"idx": 18,
|
|
133
|
+
"version": "6",
|
|
134
|
+
"when": 1784900000000,
|
|
135
|
+
"tag": "0018_default_branch",
|
|
136
|
+
"breakpoints": true
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"idx": 19,
|
|
140
|
+
"version": "6",
|
|
141
|
+
"when": 1785000000000,
|
|
142
|
+
"tag": "0019_git_provider",
|
|
143
|
+
"breakpoints": true
|
|
123
144
|
}
|
|
124
145
|
]
|
|
125
146
|
}
|
package/package.json
CHANGED
|
@@ -8,10 +8,15 @@ description: >
|
|
|
8
8
|
`**Approved**:` line (stamped by Compound Context Studio's Approve button). Produces
|
|
9
9
|
cicd/app/ — a zero-npm-dependency node:http app (server.js + cicd.config.json) rendering a
|
|
10
10
|
bare light-themed page (no header/sidebar/footer, iframe-embeddable) with one nav tab per
|
|
11
|
-
application: a controls bar (status badge, clickable app URL opening a new tab,
|
|
12
|
-
Start / Stop / Deploy buttons) above that application's log streamed live
|
|
13
|
-
zero-dependency WebSocket (bounded tail + backpressure drop server-side, trimmed
|
|
14
|
-
client-side — no leaks/OOM).
|
|
11
|
+
application: a controls bar (status badge, clickable app URL opening a new tab, a Config
|
|
12
|
+
button, and Build / Start / Stop / Deploy buttons) above that application's log streamed live
|
|
13
|
+
over a built-in zero-dependency WebSocket (bounded tail + backpressure drop server-side, trimmed
|
|
14
|
+
DOM buffer client-side — no leaks/OOM). The Config button opens a modal listing the application's
|
|
15
|
+
runtime config files (from the plan's `## Application Configs`) — each with its source
|
|
16
|
+
(ENVIRONMENT.md-derived vs an existing .env/.env-* file), present/MISSING status, required keys
|
|
17
|
+
and full live content — for troubleshooting; and before Build/Start the app materializes any
|
|
18
|
+
missing plan-managed config file into the application folder so it always starts with complete
|
|
19
|
+
configuration. Build and Deploy commands come from the approved plan (inferred
|
|
15
20
|
from ENVIRONMENT.md/DEVTOOL.md — e.g. Spring Boot jar vs Docker image; deploy via web-server
|
|
16
21
|
copy, docker run or kubectl apply, or none). Controls are disabled for any application that
|
|
17
22
|
has never completed a conductor-develop workflow run (checked against the studio's
|
|
@@ -58,12 +63,22 @@ Read `references/cicd-app-template.md` for the complete file templates. Produce:
|
|
|
58
63
|
|
|
59
64
|
1. `cicd/app/cicd.config.json` — from the plan's tables:
|
|
60
65
|
`{ "port": <CI/CD app port>, "apps": [{ "name", "folder", "method", "port", "build",
|
|
61
|
-
"start", "stop", "deployMethod", "deploy", "health" }], "infra": [{ "id", "name",
|
|
62
|
-
"port", "url", "description" }] }` — `deployMethod` is the plan's Deploy Method column
|
|
66
|
+
"start", "stop", "deployMethod", "deploy", "health", "configs" }], "infra": [{ "id", "name",
|
|
67
|
+
"host", "port", "url", "description" }] }` — `deployMethod` is the plan's Deploy Method column
|
|
63
68
|
(`none|web-server|docker|kubernetes`) and `deploy` its command (`null` when the method is
|
|
64
69
|
`none`). The `infra` array comes from the plan's `## Infrastructure` table (one entry per
|
|
65
70
|
row); `url` is `null` when the Console URL cell is `-`. Emit `"infra": []` when the plan's
|
|
66
71
|
Infrastructure table is empty — never invent services the plan does not list.
|
|
72
|
+
- `configs` — the runtime config files each application reads at start, from the plan's
|
|
73
|
+
`## Application Configs` section (one entry per file):
|
|
74
|
+
`{ "path", "label", "source", "managed", "keys", "note", "content" }`. `source` is
|
|
75
|
+
`environment` | `env-file` | `derived`; `path` is relative to the application folder. When
|
|
76
|
+
`managed` is `true`, `content` is the plan's resolved file body (verbatim, credentials
|
|
77
|
+
included per the dev-plaintext rule) — the generated app writes it into the application folder
|
|
78
|
+
before Build/Start if the file is missing, so the app always starts with COMPLETE config. For
|
|
79
|
+
an `env-file` used as-is on disk, set `managed:false` and `content:null`. Copy the plan's
|
|
80
|
+
resolved `content` blocks verbatim — NEVER drop a managed file's content (that body is what
|
|
81
|
+
guarantees the app boots). Emit `"configs": []` for an app the plan lists no config files for.
|
|
67
82
|
2. `cicd/app/server.js` — the template verbatim (it is generic; all project specifics live in
|
|
68
83
|
cicd.config.json). Zero npm dependencies.
|
|
69
84
|
3. Ensure `cicd/logs/` and `cicd/state.json` are ignored: append `cicd/logs/` and
|
|
@@ -78,6 +93,10 @@ port, application count) and STOP — the studio starts/embeds the app.
|
|
|
78
93
|
|
|
79
94
|
- Zero npm dependencies — plain node:http only (the WebSocket log stream is hand-rolled in the
|
|
80
95
|
template); no install step may be required.
|
|
96
|
+
- Live config: the generated server re-reads `cicd.config.json` on demand (mtime-cached), so a
|
|
97
|
+
regenerated plan — corrected build/start commands, a renamed build artifact, added apps or
|
|
98
|
+
changed infra — is applied WITHOUT restarting the running app (only the listen port is bound
|
|
99
|
+
once at boot). Never cache the parsed config in a top-level `const`.
|
|
81
100
|
- The page is bare: no header, sidebar or footer (it is iframed by the studio). Light theme,
|
|
82
101
|
one nav tab per application; the tab body is that application's live log.
|
|
83
102
|
- Infrastructure page: when `infra` is non-empty the main page shows an "Infrastructure" link
|
|
@@ -93,9 +112,22 @@ port, application count) and STOP — the studio starts/embeds the app.
|
|
|
93
112
|
- Develop gate: controls stay disabled for an application that never completed a
|
|
94
113
|
conductor-develop run — checked via `${CO2_STUDIO_URL}/api/cicd-guard/${CO2_PROJECT_ID}`
|
|
95
114
|
with the CO2_GUARD_TOKEN from the spawn env (refreshed every 30 s, last answer cached).
|
|
115
|
+
- Application config (troubleshooting + completeness): every application tab has a **Config**
|
|
116
|
+
button opening a modal that lists the app's runtime config files (from `configs`) — each with its
|
|
117
|
+
source badge (`environment`/`env-file`/`derived`), present/MISSING status, required keys and full
|
|
118
|
+
live on-disk content. The Config button is ALWAYS enabled (independent of the develop gate) so a
|
|
119
|
+
failed start-up can always be inspected. Before Build and before Start the server materializes any
|
|
120
|
+
`managed` config file whose `content` is set into the application folder when it is missing (never
|
|
121
|
+
overwriting a file already on disk), so an application never fails to come up for want of config.
|
|
96
122
|
- Log streaming must stay bounded: per-app log file on disk, bounded tail on connect, live
|
|
97
123
|
append broadcast only, slow subscribers dropped on backpressure, client DOM buffer trimmed.
|
|
98
|
-
- Already-running detection: `/api/state`
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
- Already-running detection: `/api/state` decides "is it running?" by a raw TCP connect to the
|
|
125
|
+
application's `port` on BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the host named
|
|
126
|
+
in its `health` URL — up if any connects. This is family-agnostic (Spring binds `127.0.0.1`,
|
|
127
|
+
Vite/Next preview bind IPv6 `::1` only) and reliable where an HTTP fetch is not, so an app
|
|
128
|
+
started outside this page still shows as running. Carry `health` and `port` from the plan
|
|
129
|
+
verbatim into `cicd.config.json`.
|
|
130
|
+
- Stop works for any running app: if the app was started outside this CI/CD app (no child
|
|
131
|
+
handle), Stop kills whatever process tree is listening on its `port` (kill-by-port), so the
|
|
132
|
+
Stop control is enabled/usable whenever an app is detected running.
|
|
101
133
|
- Never touch PRD.md/CLAUDE.md; never run builds or deploys yourself during generation.
|
|
@@ -21,8 +21,23 @@ const APP_DIR = __dirname
|
|
|
21
21
|
const PROJECT_DIR = resolve(APP_DIR, '..', '..')
|
|
22
22
|
const LOG_DIR = join(PROJECT_DIR, 'cicd', 'logs')
|
|
23
23
|
const STATE_FILE = join(PROJECT_DIR, 'cicd', 'state.json')
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const CONFIG_PATH = join(APP_DIR, 'cicd.config.json')
|
|
25
|
+
// Config is re-read on demand (mtime-cached), so a regenerated plan — corrected build/start
|
|
26
|
+
// commands, renamed artifacts, added apps or changed infra — is picked up WITHOUT restarting
|
|
27
|
+
// this app. Only the listen PORT is bound once at boot (a port change still needs a respawn).
|
|
28
|
+
let configCache = { mtimeMs: -1, data: { port: 4100, apps: [], infra: [] } }
|
|
29
|
+
function loadConfig() {
|
|
30
|
+
try {
|
|
31
|
+
const mtimeMs = statSync(CONFIG_PATH).mtimeMs
|
|
32
|
+
if (mtimeMs !== configCache.mtimeMs) {
|
|
33
|
+
configCache = { mtimeMs, data: JSON.parse(readFileSync(CONFIG_PATH, 'utf8')) }
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* unreadable or mid-write — keep the last good config */
|
|
37
|
+
}
|
|
38
|
+
return configCache.data
|
|
39
|
+
}
|
|
40
|
+
const PORT = Number(process.env.PORT || loadConfig().port || 4100)
|
|
26
41
|
const STUDIO = process.env.CO2_STUDIO_URL || 'http://127.0.0.1:3001'
|
|
27
42
|
const PROJECT_ID = process.env.CO2_PROJECT_ID || ''
|
|
28
43
|
const TOKEN = process.env.CO2_GUARD_TOKEN || ''
|
|
@@ -36,7 +51,7 @@ const lastError = new Map() // app name -> string
|
|
|
36
51
|
|
|
37
52
|
function saveState() {
|
|
38
53
|
const s = {}
|
|
39
|
-
for (const a of
|
|
54
|
+
for (const a of loadConfig().apps) s[a.name] = running.has(a.name) ? 'running' : 'stopped'
|
|
40
55
|
try { writeFileSync(STATE_FILE, JSON.stringify(s)) } catch {}
|
|
41
56
|
}
|
|
42
57
|
|
|
@@ -133,13 +148,67 @@ function runLogged(app, command, onExit) {
|
|
|
133
148
|
return child
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
// ---- Application config (runtime .env etc.) --------------------------------------------
|
|
152
|
+
// Each app declares the config files its start command reads (inferred by util-plancicd from
|
|
153
|
+
// ENVIRONMENT.md or an existing .env/.env-* file). A `managed` file carries the plan's resolved
|
|
154
|
+
// content and is materialized into the application folder before Build/Start when it is missing —
|
|
155
|
+
// so an application always starts with COMPLETE configuration. The Config modal shows every file's
|
|
156
|
+
// live on-disk content for troubleshooting. Paths are relative to the application folder.
|
|
157
|
+
const MAX_CONFIG_BYTES = 200_000
|
|
158
|
+
function configAbsPath(app, cfg) { return join(PROJECT_DIR, app.folder, cfg.path) }
|
|
159
|
+
function ensureConfigs(app) {
|
|
160
|
+
for (const cfg of app.configs || []) {
|
|
161
|
+
if (!cfg.managed || cfg.content == null) continue // env-file configs are used as-is on disk
|
|
162
|
+
const abs = configAbsPath(app, cfg)
|
|
163
|
+
if (existsSync(abs)) continue // never overwrite a config already on disk
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(join(abs, '..'), { recursive: true })
|
|
166
|
+
writeFileSync(abs, cfg.content)
|
|
167
|
+
logLine(app, `config: wrote ${cfg.path} from the plan (was missing)`)
|
|
168
|
+
} catch (e) {
|
|
169
|
+
logLine(app, `config: FAILED to write ${cfg.path} — ${e.message}`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function readConfig(app, cfg) {
|
|
174
|
+
const abs = configAbsPath(app, cfg)
|
|
175
|
+
let exists = false, content = ''
|
|
176
|
+
try {
|
|
177
|
+
if (existsSync(abs)) {
|
|
178
|
+
exists = true
|
|
179
|
+
const buf = readFileSync(abs)
|
|
180
|
+
content = buf.length > MAX_CONFIG_BYTES
|
|
181
|
+
? buf.slice(0, MAX_CONFIG_BYTES).toString('utf8') + '\n… (truncated)'
|
|
182
|
+
: buf.toString('utf8')
|
|
183
|
+
} else if (cfg.managed && cfg.content != null) {
|
|
184
|
+
content = cfg.content // planned content — not yet written to disk
|
|
185
|
+
}
|
|
186
|
+
} catch (e) { content = `(unreadable: ${e.message})` }
|
|
187
|
+
return {
|
|
188
|
+
path: cfg.path, label: cfg.label || cfg.path, source: cfg.source || 'env-file',
|
|
189
|
+
managed: !!cfg.managed, keys: cfg.keys || [], note: cfg.note || '', exists, content,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
136
193
|
// ---- App lifecycle ---------------------------------------------------------------------
|
|
194
|
+
// Candidate loopback hosts to check for liveness: IPv4 (127.0.0.1) AND IPv6 (::1) loopback
|
|
195
|
+
// always, plus whatever host the plan's health URL names. Frameworks bind different families —
|
|
196
|
+
// Spring Boot listens on 127.0.0.1, but Vite/Next preview dev servers bind IPv6 ::1 only — so
|
|
197
|
+
// probing a single family misses half of them. `localhost` in a health URL is included as-is
|
|
198
|
+
// and resolved by the OS.
|
|
199
|
+
function probeHosts(app) {
|
|
200
|
+
const hosts = new Set(['127.0.0.1', '::1'])
|
|
201
|
+
try { if (app.health) hosts.add(new URL(app.health).hostname) } catch {}
|
|
202
|
+
return [...hosts]
|
|
203
|
+
}
|
|
204
|
+
// Liveness = a raw TCP connect to the app's listen port on ANY candidate host, NOT an HTTP
|
|
205
|
+
// fetch of a health URL. A listening port means the app is up regardless of auth (Spring
|
|
206
|
+
// Security 401s), path (actuator disabled), or protocol — and it is reliable where Node's fetch
|
|
207
|
+
// is not (undici can stall on localhost keep-alive and time out against a server curl reaches
|
|
208
|
+
// in 20 ms). This also detects apps started outside this CI/CD app, so their state shows right.
|
|
137
209
|
async function probe(app) {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
await fetch(url, { signal: AbortSignal.timeout(800) })
|
|
141
|
-
return true
|
|
142
|
-
} catch { return false }
|
|
210
|
+
const results = await Promise.all(probeHosts(app).map((h) => tcpProbe(h, app.port, 800)))
|
|
211
|
+
return results.some(Boolean)
|
|
143
212
|
}
|
|
144
213
|
|
|
145
214
|
// Infrastructure reachability — a raw TCP connect, so it works for non-HTTP services
|
|
@@ -160,6 +229,7 @@ function tcpProbe(host, port, timeout = 1000) {
|
|
|
160
229
|
function buildApp(app) {
|
|
161
230
|
if (activity.has(app.name)) return
|
|
162
231
|
activity.set(app.name, 'building'); lastError.delete(app.name)
|
|
232
|
+
ensureConfigs(app)
|
|
163
233
|
runLogged(app, app.build, (code) => {
|
|
164
234
|
activity.delete(app.name)
|
|
165
235
|
if (code !== 0) lastError.set(app.name, `build exited ${code} — see log`)
|
|
@@ -180,6 +250,7 @@ function deployApp(app) {
|
|
|
180
250
|
function startApp(app) {
|
|
181
251
|
if (running.get(app.name) || activity.has(app.name)) return
|
|
182
252
|
failed.delete(app.name); lastError.delete(app.name)
|
|
253
|
+
ensureConfigs(app)
|
|
183
254
|
if (app.method === 'container') {
|
|
184
255
|
runLogged(app, app.start, () => {})
|
|
185
256
|
saveState(); return
|
|
@@ -207,17 +278,45 @@ function killTree(child) {
|
|
|
207
278
|
}
|
|
208
279
|
}
|
|
209
280
|
|
|
281
|
+
// Stop an app we DON'T hold a child handle for (started outside this app, or before this app
|
|
282
|
+
// last restarted) by killing whatever process tree is listening on its port — so Stop works
|
|
283
|
+
// for any running app, not only ones this CI/CD app spawned. Returns true if a PID was killed.
|
|
284
|
+
function killByPort(port) {
|
|
285
|
+
try {
|
|
286
|
+
if (process.platform === 'win32') {
|
|
287
|
+
const out = execFileSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8' })
|
|
288
|
+
const pids = new Set()
|
|
289
|
+
for (const line of out.split(/\r?\n/)) {
|
|
290
|
+
const m = line.match(/:(\d+)\s+\S+\s+LISTENING\s+(\d+)/)
|
|
291
|
+
if (m && Number(m[1]) === Number(port)) pids.add(m[2])
|
|
292
|
+
}
|
|
293
|
+
for (const pid of pids) {
|
|
294
|
+
try { execFileSync('taskkill', ['/pid', pid, '/T', '/F'], { stdio: 'ignore' }) } catch {}
|
|
295
|
+
}
|
|
296
|
+
return pids.size > 0
|
|
297
|
+
}
|
|
298
|
+
const out = execFileSync('sh', ['-c', `lsof -ti tcp:${port} || true`], { encoding: 'utf8' })
|
|
299
|
+
const pids = out.split(/\s+/).filter(Boolean)
|
|
300
|
+
for (const pid of pids) { try { process.kill(Number(pid)) } catch {} }
|
|
301
|
+
return pids.length > 0
|
|
302
|
+
} catch { return false }
|
|
303
|
+
}
|
|
304
|
+
|
|
210
305
|
function stopApp(app) {
|
|
211
306
|
if (app.method === 'container' && app.stop && app.stop !== 'process-kill') {
|
|
212
307
|
try { execFileSync(app.stop.split(' ')[0], app.stop.split(' ').slice(1), { cwd: PROJECT_DIR }) } catch {}
|
|
213
308
|
} else {
|
|
214
309
|
const child = running.get(app.name)
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
310
|
+
if (child) {
|
|
311
|
+
stopping.add(app.name)
|
|
312
|
+
killTree(child)
|
|
313
|
+
} else {
|
|
314
|
+
// No child handle — kill by port so an externally/previously started app still stops.
|
|
315
|
+
lastError.delete(app.name)
|
|
316
|
+
if (!killByPort(app.port)) {
|
|
317
|
+
lastError.set(app.name, `could not stop — nothing found listening on port ${app.port}`)
|
|
318
|
+
}
|
|
218
319
|
}
|
|
219
|
-
stopping.add(app.name)
|
|
220
|
-
killTree(child)
|
|
221
320
|
}
|
|
222
321
|
running.delete(app.name)
|
|
223
322
|
saveState()
|
|
@@ -248,6 +347,19 @@ button:disabled{opacity:.4;cursor:default}
|
|
|
248
347
|
.notice{padding:4px 12px;font-size:11px;color:#8a5a00;background:#fff7e6;display:none}
|
|
249
348
|
.err{padding:4px 12px;font-size:11px;color:#c23b3b;display:none;white-space:pre-wrap}
|
|
250
349
|
#log{flex:1;margin:0;padding:10px 12px;background:#0f1216;color:#d7dde4;font-size:11px;line-height:1.5;overflow:auto;white-space:pre-wrap}
|
|
350
|
+
.muted{color:#5b6572;font-size:12px}
|
|
351
|
+
.modal{position:fixed;inset:0;background:rgba(0,0,0,.45);display:none;align-items:center;justify-content:center;padding:24px;z-index:20}
|
|
352
|
+
.modal.open{display:flex}
|
|
353
|
+
.modal-panel{background:#fff;border-radius:10px;max-width:920px;width:100%;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.3)}
|
|
354
|
+
.modal-head{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid #e1e5ea;font-size:13px}
|
|
355
|
+
.modal-body{padding:12px 14px;overflow:auto}
|
|
356
|
+
.cfg{margin-bottom:16px}
|
|
357
|
+
.cfg h3{font-size:13px;margin:0 0 4px;display:flex;gap:8px;align-items:center}
|
|
358
|
+
.cfg .src{font-size:10px;text-transform:uppercase;border-radius:99px;padding:2px 8px;background:#eef1f5;color:#5b6572}
|
|
359
|
+
.cfg .miss{color:#c23b3b;font-weight:600}
|
|
360
|
+
.cfg .ok{color:#1c8a4a;font-weight:600}
|
|
361
|
+
.cfg .keys{font-size:11px;color:#5b6572;margin:2px 0}
|
|
362
|
+
.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}
|
|
251
363
|
</style></head><body>
|
|
252
364
|
<div class="top"><strong>CI/CD</strong><span id="branch" class="meta"></span><span style="flex:1"></span><a id="infralink" class="url" href="/infra" target="_blank" rel="noreferrer" style="display:none">Infrastructure ↗</a></div>
|
|
253
365
|
<div class="tabs" id="tabs"></div>
|
|
@@ -255,7 +367,14 @@ button:disabled{opacity:.4;cursor:default}
|
|
|
255
367
|
<div class="notice" id="notice"></div>
|
|
256
368
|
<div class="err" id="err"></div>
|
|
257
369
|
<pre id="log"></pre>
|
|
370
|
+
<div class="modal" id="cfgModal" onclick="if(event.target===this)closeConfig()">
|
|
371
|
+
<div class="modal-panel">
|
|
372
|
+
<div class="modal-head"><strong id="cfgTitle">Configuration</strong><span style="flex:1"></span><button onclick="closeConfig()">Close</button></div>
|
|
373
|
+
<div class="modal-body" id="cfgBody"></div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
258
376
|
<script>
|
|
377
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])) }
|
|
259
378
|
let state = { apps: [] }
|
|
260
379
|
let active = null
|
|
261
380
|
let ws = null, wsTimer = null
|
|
@@ -279,9 +398,10 @@ function renderBar(){
|
|
|
279
398
|
<span class="meta">\${a.method} · port \${a.port}</span>
|
|
280
399
|
<a class="url" href="\${url}" target="_blank" rel="noreferrer">\${url}</a>
|
|
281
400
|
<span style="flex:1"></span>
|
|
401
|
+
<button onclick="openConfig()" title="Show this application's runtime configuration files">Config</button>
|
|
282
402
|
<button onclick="act('build')" \${!gate||busy||a.phase==='running'?'disabled':''}>Build</button>
|
|
283
403
|
<button onclick="act('start')" \${!gate||busy||a.phase==='running'?'disabled':''}>Start</button>
|
|
284
|
-
<button onclick="act('stop')" \${a.phase!=='running'
|
|
404
|
+
<button onclick="act('stop')" \${a.phase!=='running'?'disabled':''}>Stop</button>
|
|
285
405
|
\${a.deploy?\`<button onclick="act('deploy')" \${!gate||busy?'disabled':''}>Deploy (\${a.deployMethod})</button>\`:''}\`
|
|
286
406
|
const notice = document.getElementById('notice')
|
|
287
407
|
if (a.developedOnce === false) {
|
|
@@ -323,6 +443,30 @@ function appendLog(text){
|
|
|
323
443
|
if (atBottom) el.scrollTop = el.scrollHeight
|
|
324
444
|
}
|
|
325
445
|
|
|
446
|
+
// Config modal — the runtime config files this application needs to start (from the plan). Shows
|
|
447
|
+
// each file's live on-disk content (or the planned content when a managed file is not yet written),
|
|
448
|
+
// its source (environment / env-file / derived), required keys and present/MISSING status — so the
|
|
449
|
+
// user can troubleshoot a start-up that is failing for want of complete configuration.
|
|
450
|
+
async function openConfig(){
|
|
451
|
+
if (!active) return
|
|
452
|
+
const modal = document.getElementById('cfgModal')
|
|
453
|
+
document.getElementById('cfgTitle').textContent = 'Configuration — '+active
|
|
454
|
+
document.getElementById('cfgBody').innerHTML = '<div class="muted">Loading…</div>'
|
|
455
|
+
modal.classList.add('open')
|
|
456
|
+
const s = await fetch('/api/apps/'+encodeURIComponent(active)+'/config').then(r=>r.json()).catch(()=>null)
|
|
457
|
+
const configs = (s && s.configs) || []
|
|
458
|
+
document.getElementById('cfgBody').innerHTML = configs.length ? configs.map(c => \`
|
|
459
|
+
<div class="cfg">
|
|
460
|
+
<h3>\${esc(c.label)} <span class="src">\${esc(c.source)}</span>
|
|
461
|
+
\${c.exists ? '<span class="ok">present</span>' : '<span class="miss">MISSING'+(c.managed&&c.content?' — created on Build/Start':'')+'</span>'}</h3>
|
|
462
|
+
\${c.keys&&c.keys.length ? '<div class="keys">Required keys: '+c.keys.map(esc).join(', ')+'</div>' : ''}
|
|
463
|
+
\${c.note ? '<div class="keys">'+esc(c.note)+'</div>' : ''}
|
|
464
|
+
<pre>\${esc(c.content) || '(empty)'}</pre>
|
|
465
|
+
</div>\`).join('') : '<div class="muted">No runtime configuration declared for this application.</div>'
|
|
466
|
+
}
|
|
467
|
+
function closeConfig(){ document.getElementById('cfgModal').classList.remove('open') }
|
|
468
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeConfig() })
|
|
469
|
+
|
|
326
470
|
async function refresh(){
|
|
327
471
|
const s = await fetch('/api/state').then(r => r.json()).catch(() => null)
|
|
328
472
|
if (!s) return
|
|
@@ -336,6 +480,10 @@ async function act(op){
|
|
|
336
480
|
await fetch('/api/apps/'+encodeURIComponent(active)+'/'+op, { method: 'POST' })
|
|
337
481
|
refresh()
|
|
338
482
|
}
|
|
483
|
+
// Rescan immediately when the user returns to this page (tab focus / regained visibility), so
|
|
484
|
+
// coming back from another screen reflects the true app + infra state without waiting the poll.
|
|
485
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh() })
|
|
486
|
+
window.addEventListener('focus', refresh)
|
|
339
487
|
refresh(); setInterval(refresh, 3000)
|
|
340
488
|
</script></body></html>`
|
|
341
489
|
|
|
@@ -373,6 +521,10 @@ async function refresh(){
|
|
|
373
521
|
</tr>\`).join('')
|
|
374
522
|
document.getElementById('rows').innerHTML = rows || '<tr><td colspan="4" class="muted">No infrastructure declared.</td></tr>'
|
|
375
523
|
}
|
|
524
|
+
// Rescan immediately when the user returns to this page (tab focus / regained visibility), so
|
|
525
|
+
// coming back from another screen reflects the true app + infra state without waiting the poll.
|
|
526
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh() })
|
|
527
|
+
window.addEventListener('focus', refresh)
|
|
376
528
|
refresh(); setInterval(refresh, 3000)
|
|
377
529
|
</script></body></html>`
|
|
378
530
|
|
|
@@ -387,7 +539,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
387
539
|
return res.end(INFRA_PAGE)
|
|
388
540
|
}
|
|
389
541
|
if (req.method === 'GET' && req.url === '/api/infra') {
|
|
390
|
-
const infra = await Promise.all((
|
|
542
|
+
const infra = await Promise.all((loadConfig().infra || []).map(async (i) => ({
|
|
391
543
|
id: i.id, name: i.name, host: i.host || '127.0.0.1', port: i.port,
|
|
392
544
|
url: i.url || null, description: i.description || '',
|
|
393
545
|
up: await tcpProbe(i.host || '127.0.0.1', i.port),
|
|
@@ -395,7 +547,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
395
547
|
return json(res, 200, { infra })
|
|
396
548
|
}
|
|
397
549
|
if (req.method === 'GET' && req.url === '/api/state') {
|
|
398
|
-
const apps = await Promise.all(
|
|
550
|
+
const apps = await Promise.all(loadConfig().apps.map(async (a) => {
|
|
399
551
|
const healthy = await probe(a)
|
|
400
552
|
const phase = activity.get(a.name) || (healthy ? 'running' : failed.has(a.name) ? 'failed' : 'stopped')
|
|
401
553
|
return {
|
|
@@ -406,11 +558,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
406
558
|
error: lastError.get(a.name) || null,
|
|
407
559
|
}
|
|
408
560
|
}))
|
|
409
|
-
return json(res, 200, { currentBranch: currentBranch(), hasInfra: (
|
|
561
|
+
return json(res, 200, { currentBranch: currentBranch(), hasInfra: (loadConfig().infra || []).length > 0, apps })
|
|
562
|
+
}
|
|
563
|
+
const cfgm = req.url && req.url.match(/^\/api\/apps\/([^/]+)\/config$/)
|
|
564
|
+
if (cfgm && req.method === 'GET') {
|
|
565
|
+
const app = loadConfig().apps.find((a) => a.name === decodeURIComponent(cfgm[1]))
|
|
566
|
+
if (!app) return json(res, 404, { error: 'unknown app' })
|
|
567
|
+
return json(res, 200, { configs: (app.configs || []).map((c) => readConfig(app, c)) })
|
|
410
568
|
}
|
|
411
569
|
const m = req.url && req.url.match(/^\/api\/apps\/([^/]+)\/(build|start|stop|deploy)$/)
|
|
412
570
|
if (m && req.method === 'POST') {
|
|
413
|
-
const app =
|
|
571
|
+
const app = loadConfig().apps.find((a) => a.name === decodeURIComponent(m[1]))
|
|
414
572
|
if (!app) return json(res, 404, { error: 'unknown app' })
|
|
415
573
|
if (m[2] !== 'stop' && developedOnce(app) === false) {
|
|
416
574
|
return json(res, 409, { error: 'application has not completed a develop workflow run' })
|
|
@@ -434,7 +592,7 @@ server.on('upgrade', (req, socket) => {
|
|
|
434
592
|
name = url.searchParams.get('app') || ''
|
|
435
593
|
} catch {}
|
|
436
594
|
const key = req.headers['sec-websocket-key']
|
|
437
|
-
const app =
|
|
595
|
+
const app = loadConfig().apps.find((a) => a.name === name)
|
|
438
596
|
if (!app || !key || !isWsPath) { socket.destroy(); return }
|
|
439
597
|
const accept = createHash('sha1').update(key + WS_GUID).digest('base64')
|
|
440
598
|
socket.write(
|
|
@@ -459,7 +617,18 @@ server.listen(PORT, () => console.log(`cicd app on :${PORT}`))
|
|
|
459
617
|
Decisions table, one `infra` entry per row of the plan's `## Infrastructure` table.
|
|
460
618
|
`deployMethod`/`deploy` come from the plan's Deploy columns (`deploy` is `null` when the method
|
|
461
619
|
is `none`). `infra[].url` is `null` for a headless service (Console URL cell `-`); emit
|
|
462
|
-
`"infra": []` when the plan declares no supporting services
|
|
620
|
+
`"infra": []` when the plan declares no supporting services.
|
|
621
|
+
|
|
622
|
+
Each app also carries a `configs` array — the runtime config files the application reads at start,
|
|
623
|
+
from the plan's `## Application Configs` section (one entry per file). Fields:
|
|
624
|
+
`path` (relative to the application folder), `label`, `source` (`environment` | `env-file` |
|
|
625
|
+
`derived`), `managed` (when `true` the server writes `content` to `path` before Build/Start if the
|
|
626
|
+
file is missing, so start-up always has complete config), `keys` (the required keys the app needs
|
|
627
|
+
to boot), `note`, and `content` (the plan's resolved file body when `managed`; `null`/omitted for an
|
|
628
|
+
`env-file` used as-is on disk). Emit `"configs": []` when an application needs no runtime config
|
|
629
|
+
file. NEVER omit a `managed.content` that the plan resolved — that body is what guarantees the app
|
|
630
|
+
starts configured. Per the dev-environment plaintext rule, `content` may include credentials
|
|
631
|
+
verbatim (this is a local dev CI/CD app).
|
|
463
632
|
|
|
464
633
|
```json
|
|
465
634
|
{
|
|
@@ -475,7 +644,27 @@ is `none`). `infra[].url` is `null` for a headless service (Console URL cell `-`
|
|
|
475
644
|
"stop": "process-kill",
|
|
476
645
|
"deployMethod": "kubernetes",
|
|
477
646
|
"deploy": "kubectl apply -f my-app/k8s/",
|
|
478
|
-
"health": "http://127.0.0.1:3000/"
|
|
647
|
+
"health": "http://127.0.0.1:3000/",
|
|
648
|
+
"configs": [
|
|
649
|
+
{
|
|
650
|
+
"path": ".env",
|
|
651
|
+
"label": ".env (runtime)",
|
|
652
|
+
"source": "environment",
|
|
653
|
+
"managed": true,
|
|
654
|
+
"keys": ["DATABASE_URL", "REDIS_HOST", "PORT"],
|
|
655
|
+
"note": "Resolved from ENVIRONMENT.md → my-app section",
|
|
656
|
+
"content": "DATABASE_URL=postgres://app:app@127.0.0.1:5432/appdb\nREDIS_HOST=127.0.0.1\nPORT=3000\n"
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
"path": ".env-uat",
|
|
660
|
+
"label": ".env-uat (existing)",
|
|
661
|
+
"source": "env-file",
|
|
662
|
+
"managed": false,
|
|
663
|
+
"keys": ["DATABASE_URL", "REDIS_HOST"],
|
|
664
|
+
"note": "Existing env file in the app folder — used as-is",
|
|
665
|
+
"content": null
|
|
666
|
+
}
|
|
667
|
+
]
|
|
479
668
|
}
|
|
480
669
|
],
|
|
481
670
|
"infra": [
|
|
@@ -10,7 +10,12 @@ description: >
|
|
|
10
10
|
method (container vs bare process), build command (e.g. Spring Boot jar vs Docker image),
|
|
11
11
|
start command, stop mechanism, deploy method + command (none / web-server copy / docker run /
|
|
12
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. It also
|
|
13
|
+
port for the generated CI/CD app itself. It also infers each application's RUNTIME CONFIG — the
|
|
14
|
+
config files its start command reads (e.g. `.env`), deciding per file whether the values come
|
|
15
|
+
from ENVIRONMENT.md or from an existing `.env` / `.env-*` file already in the app folder, listing
|
|
16
|
+
the required keys and, for ENVIRONMENT.md-sourced files, the resolved content — so the generated
|
|
17
|
+
CI/CD app can complete the config before start (guaranteeing the app comes up) and show it in a
|
|
18
|
+
per-application Config modal for troubleshooting. It also enumerates the project's supporting
|
|
14
19
|
infrastructure (3rd-party services from CLAUDE.md's `# Supporting 3rd Party Applications` and
|
|
15
20
|
ENVIRONMENT.md — host, port and optional console URL) into an `## Infrastructure` section so
|
|
16
21
|
the generated CI/CD app can surface a status-only Infrastructure page. The plan carries a
|
|
@@ -47,6 +52,7 @@ If the argument is missing or the file does not exist, STOP and tell the user to
|
|
|
47
52
|
| Dev tools | `DEVTOOL.md` (project root) |
|
|
48
53
|
| Project context | `CLAUDE.md` (project root; `source/CLAUDE.md` fallback) |
|
|
49
54
|
| App specification | `<app_folder>/context/specification/SPECIFICATION.md` |
|
|
55
|
+
| App config files | `<app_folder>/.env`, `<app_folder>/.env-*` (and stack-conventional config files) |
|
|
50
56
|
| Infrastructure | `CLAUDE.md` `# Supporting 3rd Party Applications` + `ENVIRONMENT.md` |
|
|
51
57
|
| Output plan | `cicd/CICD_PLAN.md` |
|
|
52
58
|
|
|
@@ -61,6 +67,13 @@ If the argument is missing or the file does not exist, STOP and tell the user to
|
|
|
61
67
|
4. For each application, read its SPECIFICATION.md tech-stack section if present; otherwise
|
|
62
68
|
infer the stack from build files (`package.json` → node, `pom.xml` → spring-boot,
|
|
63
69
|
`composer.json` → laravel).
|
|
70
|
+
4a. For each application, discover its RUNTIME CONFIG files: list any existing `.env` / `.env-*`
|
|
71
|
+
in the application folder AND the stack-conventional config the start command reads (node/
|
|
72
|
+
laravel → `.env`; Spring Boot → `application.properties` / `application-<profile>.yml` or a
|
|
73
|
+
`.env` when the app loads one). Read the content of each existing file (values may be reused).
|
|
74
|
+
Cross-reference ENVIRONMENT.md for the per-application config values (DB/cache/broker hosts and
|
|
75
|
+
ports, external-service URLs, credentials, secrets, the app's own PORT) and the Infrastructure
|
|
76
|
+
list from step 5 for the hosts/ports the app must point at.
|
|
64
77
|
5. Build the **infrastructure list**: one entry per supporting 3rd-party service the project
|
|
65
78
|
declares. For each, resolve `host` (default `127.0.0.1`), `port` and — when it exposes a web
|
|
66
79
|
console/UI — a `url`, cross-referencing ENVIRONMENT.md for the actual host/port/endpoint. A
|
|
@@ -96,9 +109,47 @@ If the argument is missing or the file does not exist, STOP and tell the user to
|
|
|
96
109
|
needs no deploy step beyond Start; never invent a target the config does not name.
|
|
97
110
|
- **Port**: from `# Port Allocation`; if absent, assign sequentially from 3000 avoiding
|
|
98
111
|
collisions with the studio (3001) and the CI/CD app port.
|
|
99
|
-
- **Health**:
|
|
112
|
+
- **Health / running-detection**: the endpoint the generated CI/CD app uses to tell whether the
|
|
113
|
+
application is already running. Detection is a **raw TCP connect to the app's port on loopback**
|
|
114
|
+
— the generated app probes BOTH IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback, plus the host
|
|
115
|
+
named in this Health value — so it does not matter which family the framework binds. This
|
|
116
|
+
matters because dev servers differ: Spring Boot binds `127.0.0.1`, but **Vite / Next preview
|
|
117
|
+
servers bind IPv6 `::1` (localhost) only** and are invisible to an IPv4-only probe. Therefore
|
|
118
|
+
set the Health host to **`localhost`** (not `127.0.0.1`) for any Node/Vite/Next dev-server app
|
|
119
|
+
so the detection host set includes the interface it actually binds; use `127.0.0.1` for JVM
|
|
120
|
+
apps. Value form: `http://localhost:<port>/` or a documented health path from the spec (the
|
|
121
|
+
path is informational — detection keys on the port, not the path).
|
|
100
122
|
- **CI/CD app port**: 4100 unless taken in `# Port Allocation`; record it in the plan.
|
|
101
123
|
|
|
124
|
+
### Application config (per application)
|
|
125
|
+
|
|
126
|
+
Decide the runtime config the application needs so it STARTS SUCCESSFULLY, and where each value
|
|
127
|
+
comes from. For every config file the start command reads (`path`, relative to the application
|
|
128
|
+
folder):
|
|
129
|
+
|
|
130
|
+
- **source** — classify each file:
|
|
131
|
+
- `env-file` — an existing `.env` / `.env-*` in the app folder already holds the runtime values;
|
|
132
|
+
use it as-is on disk (do NOT re-materialize it). Set `managed: no`, no resolved content.
|
|
133
|
+
- `environment` — there is no complete runtime file; resolve the required values from
|
|
134
|
+
ENVIRONMENT.md (and the Infrastructure hosts/ports) and record the fully-resolved file content.
|
|
135
|
+
Set `managed: yes` so the CI/CD app writes it before Build/Start when missing.
|
|
136
|
+
- `derived` — the runtime file is chosen/combined from existing material (e.g. copy the target
|
|
137
|
+
env's `.env-<env>` to the canonical `.env` the start command reads, or merge an existing
|
|
138
|
+
`.env-*` with ENVIRONMENT.md values). Set `managed: yes` and record the resolved content.
|
|
139
|
+
- **required keys** — enumerate every key the app must have to boot: its own PORT, datastore/cache/
|
|
140
|
+
broker connection strings, external-service URLs, credentials/secrets. Cross-check them against
|
|
141
|
+
ENVIRONMENT.md and the Infrastructure list.
|
|
142
|
+
- **completeness (the point of this)** — every required key MUST be satisfiable from ENVIRONMENT.md
|
|
143
|
+
or an existing env file. If a required key cannot be resolved, still emit the config entry but
|
|
144
|
+
call out the unresolved key in that application's Rationale as a GAP (so the human fixes
|
|
145
|
+
ENVIRONMENT.md before approving) — never silently drop it.
|
|
146
|
+
- **managed content** — for a `managed: yes` file, write the resolved body verbatim into the plan
|
|
147
|
+
as a fenced block (`path=<file>`). Values (including credentials) are emitted verbatim under the
|
|
148
|
+
dev-environment plaintext rule; they must never appear in the Rationale narrative.
|
|
149
|
+
|
|
150
|
+
Only list files the app actually reads at runtime — do not invent config files. An app that needs
|
|
151
|
+
no config file gets an empty config list.
|
|
152
|
+
|
|
102
153
|
### Infrastructure (status-only)
|
|
103
154
|
|
|
104
155
|
Supporting 3rd-party services are **not** built/started/deployed by the CI/CD app — they are
|
|
@@ -116,9 +167,10 @@ Never emit credentials into the infrastructure entries — status and links only
|
|
|
116
167
|
|
|
117
168
|
## Phase 3 — Write the plan
|
|
118
169
|
|
|
119
|
-
Create `cicd/` if missing and write `cicd/CICD_PLAN.md
|
|
170
|
+
Create `cicd/` if missing and write `cicd/CICD_PLAN.md` (shown here in a 4-backtick fence so the
|
|
171
|
+
inner ```env config block renders — the plan file itself uses a normal 3-backtick env fence):
|
|
120
172
|
|
|
121
|
-
|
|
173
|
+
````markdown
|
|
122
174
|
# CI/CD Deployment Plan — <Project Name>
|
|
123
175
|
|
|
124
176
|
**Generated**: <ISO date>
|
|
@@ -135,6 +187,37 @@ Create `cicd/` if missing and write `cicd/CICD_PLAN.md`:
|
|
|
135
187
|
|---|---|---|---|---|---|---|---|---|---|
|
|
136
188
|
| <name> | <folder> | process|container | <port> | `<cmd>` | `<cmd>` | process-kill|docker stop | none|web-server|docker|kubernetes | `<cmd or ->` | <url> |
|
|
137
189
|
|
|
190
|
+
> **Running-detection**: the generated CI/CD app determines "is it running?" by a TCP connect to
|
|
191
|
+
> each application's **Port** on both IPv4 (`127.0.0.1`) and IPv6 (`::1`) loopback plus the
|
|
192
|
+
> **Health** host — so an app is detected whichever family it binds. Use a `localhost` Health host
|
|
193
|
+
> for Node/Vite/Next apps (they bind IPv6 `::1`) and `127.0.0.1` for JVM apps.
|
|
194
|
+
|
|
195
|
+
## Application Configs
|
|
196
|
+
|
|
197
|
+
The runtime config each application needs to START SUCCESSFULLY. The generated CI/CD app writes any
|
|
198
|
+
`managed=yes` file into the application folder before Build/Start when it is missing, and shows
|
|
199
|
+
every file (source, status, required keys, live content) in the per-application Config modal for
|
|
200
|
+
troubleshooting. One table per application; `Path` is relative to the application folder.
|
|
201
|
+
|
|
202
|
+
### <App Name> (<folder>)
|
|
203
|
+
|
|
204
|
+
| Path | Source | Managed | Required Keys |
|
|
205
|
+
|---|---|---|---|
|
|
206
|
+
| .env | environment | yes | DATABASE_URL, REDIS_HOST, PORT |
|
|
207
|
+
| .env-uat | env-file | no | DATABASE_URL, REDIS_HOST |
|
|
208
|
+
|
|
209
|
+
For each `Managed=yes` row, follow it with the resolved file body (verbatim — credentials allowed
|
|
210
|
+
under the dev-plaintext rule):
|
|
211
|
+
|
|
212
|
+
```env path=.env
|
|
213
|
+
DATABASE_URL=postgres://app:app@127.0.0.1:5432/appdb
|
|
214
|
+
REDIS_HOST=127.0.0.1
|
|
215
|
+
PORT=3000
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Omit the fenced block for `env-file` rows (the file is used as-is on disk). If an application needs
|
|
219
|
+
no config file, write `_No runtime config files._` under its heading.
|
|
220
|
+
|
|
138
221
|
## Infrastructure
|
|
139
222
|
|
|
140
223
|
Supporting 3rd-party services shown status-only (TCP reachability) on the CI/CD app's
|
|
@@ -147,7 +230,7 @@ Infrastructure page. Leave the table body empty when the project declares no sup
|
|
|
147
230
|
## Rationale
|
|
148
231
|
|
|
149
232
|
- <per-application: why this method/commands, citing ENVIRONMENT.md/DEVTOOL.md evidence>
|
|
150
|
-
|
|
233
|
+
````
|
|
151
234
|
|
|
152
235
|
Then flip `**Status**: IN PROGRESS` to `**Status**: COMPLETED`. The studio watcher advances
|
|
153
236
|
only on COMPLETED.
|