Skip to content

Commit 2647233

Browse files
committed
feat(configure): make pipeline options template-, stage-, and provider-aware
- Align template dropdown values with backend-supported templates - Rehydrate pipeline options when template changes to prevent invalid configs - Conditionally render runtime, install, test, and build inputs based on: - selected template (node_app, python_app, container_service) - enabled stages (build, test, deploy) - Hide provider-specific deployment fields unless deploy stage is enabled - Fix pipeline generator to emit runtime-specific setup steps: - Use setup-node only for node_app - Use setup-python only for python_app - Prevent Node.js steps from leaking into Python-generated YAML This ensures UI state, generated YAML, and backend expectations stay in sync and removes misleading or non-applicable configuration paths.
1 parent 621c2c1 commit 2647233

2 files changed

Lines changed: 179 additions & 50 deletions

File tree

client/src/pages/ConfigurePage.tsx

Lines changed: 130 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,15 @@ export default function ConfigurePage() {
7070
alert("Pick a repo + branch on the Connect page first.");
7171
return;
7272
}
73-
await regenerate({ repo, branch });
73+
74+
await regenerate({
75+
repo,
76+
branch,
77+
template,
78+
provider,
79+
stages,
80+
options,
81+
});
7482
};
7583

7684
const handleOpenPr = async () => {
@@ -95,6 +103,36 @@ export default function ConfigurePage() {
95103
const trimmed = chatInput.trim();
96104
if (!trimmed) return;
97105

106+
// --- Sync AI intent with pipeline stages BEFORE sending to backend ---
107+
// The AI is a planner, not an authority. UI state must be updated first.
108+
const lower = trimmed.toLowerCase();
109+
110+
// Reset to defaults first
111+
let nextStages: Array<"build" | "test" | "deploy"> = ["build", "test", "deploy"];
112+
113+
if (lower.includes("just build") || lower.includes("only build")) {
114+
nextStages = ["build"];
115+
} else if (
116+
lower.includes("build and test") ||
117+
(lower.includes("build") && lower.includes("test") && !lower.includes("deploy"))
118+
) {
119+
nextStages = ["build", "test"];
120+
} else if (
121+
lower.includes("no deploy") ||
122+
lower.includes("without deploy")
123+
) {
124+
nextStages = ["build", "test"];
125+
}
126+
127+
// Apply stage changes to the pipeline store
128+
(["build", "test", "deploy"] as const).forEach((stage) => {
129+
const shouldEnable = nextStages.includes(stage);
130+
const isEnabled = stages.includes(stage);
131+
if (shouldEnable !== isEnabled) {
132+
toggleStage(stage);
133+
}
134+
});
135+
98136
if (!repo || !branch) {
99137
alert(
100138
"Pick a repo + branch on the Connect page first so I can give better suggestions."
@@ -114,7 +152,7 @@ export default function ConfigurePage() {
114152
template,
115153
provider,
116154
branch,
117-
stages,
155+
stages: nextStages,
118156
options,
119157
};
120158

@@ -159,9 +197,8 @@ export default function ConfigurePage() {
159197
pipelineName,
160198
branch,
161199
provider,
162-
stages,
163-
// Keep a copy of the current options in wizard context so follow-up prompts
164-
// can reference the selected provider identity (AWS role / GCP service account).
200+
// 🔒 Never override stages from backend / metadata
201+
stages: pipelineSnapshot.stages,
165202
options,
166203
} as any);
167204
}
@@ -249,8 +286,8 @@ export default function ConfigurePage() {
249286
className="rounded-md border border-white/25 bg-white px-3 py-2 text-sm text-slate-900 placeholder-slate-500"
250287
>
251288
<option value="node_app">Node.js app</option>
252-
<option value="node_library">Node.js library</option>
253-
<option value="react_vite">React/Vite app</option>
289+
<option value="python_app">Python App</option>
290+
<option value="container_service">Container</option>
254291
</select>
255292
<span className="text-xs text-slate-200">
256293
Pick the closest match to your repo; the MCP backend refines it.
@@ -296,54 +333,98 @@ export default function ConfigurePage() {
296333
</div>
297334
</fieldset>
298335

299-
{/* Node version + commands */}
336+
{/* Runtime version + commands */}
300337
<div className="grid gap-4">
301-
<label className="grid gap-1">
302-
<span className="text-sm font-medium text-slate-800">Node version</span>
303-
<input
304-
disabled={busy}
305-
value={options.nodeVersion}
306-
onChange={(e) => setOption("nodeVersion", e.target.value)}
307-
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
308-
placeholder="20"
309-
/>
310-
</label>
338+
{/* Node.js version: only show for node_app AND build stage enabled */}
339+
{template === "node_app" && stages.includes("build") && (
340+
<label className="grid gap-1">
341+
<span className="text-sm font-medium text-slate-800">Node.js version</span>
342+
<input
343+
disabled={busy}
344+
value={options.nodeVersion}
345+
onChange={(e) => setOption("nodeVersion", e.target.value)}
346+
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
347+
placeholder="20"
348+
/>
349+
</label>
350+
)}
311351

312-
<label className="grid gap-1">
313-
<span className="text-sm font-medium text-slate-800">Install command</span>
314-
<input
315-
disabled={busy}
316-
value={options.installCmd}
317-
onChange={(e) => setOption("installCmd", e.target.value)}
318-
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
319-
placeholder="npm ci"
320-
/>
321-
</label>
352+
{/* Install command: only show if build stage enabled */}
353+
{stages.includes("build") && (
354+
<label className="grid gap-1">
355+
<span className="text-sm font-medium text-slate-800">
356+
{template === "node_app"
357+
? "Install command (npm)"
358+
: template === "python_app"
359+
? "Install command (pip)"
360+
: "Install command"}
361+
</span>
362+
<input
363+
disabled={busy}
364+
value={options.installCmd}
365+
onChange={(e) => setOption("installCmd", e.target.value)}
366+
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
367+
placeholder={
368+
template === "node_app"
369+
? "npm ci"
370+
: template === "python_app"
371+
? "pip install -r requirements.txt"
372+
: ""
373+
}
374+
/>
375+
</label>
376+
)}
322377

323-
<label className="grid gap-1">
324-
<span className="text-sm font-medium text-slate-800">Test command</span>
325-
<input
326-
disabled={busy}
327-
value={options.testCmd}
328-
onChange={(e) => setOption("testCmd", e.target.value)}
329-
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
330-
placeholder="npm test"
331-
/>
332-
</label>
378+
{/* Test command: only show if test stage enabled */}
379+
{stages.includes("test") && (
380+
<label className="grid gap-1">
381+
<span className="text-sm font-medium text-slate-800">
382+
{template === "node_app"
383+
? "Test command (npm)"
384+
: template === "python_app"
385+
? "Test command (pytest)"
386+
: "Test command"}
387+
</span>
388+
<input
389+
disabled={busy}
390+
value={options.testCmd}
391+
onChange={(e) => setOption("testCmd", e.target.value)}
392+
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
393+
placeholder={
394+
template === "node_app"
395+
? "npm test"
396+
: template === "python_app"
397+
? "pytest"
398+
: ""
399+
}
400+
/>
401+
</label>
402+
)}
333403

334-
<label className="grid gap-1">
335-
<span className="text-sm font-medium text-slate-800">Build command</span>
336-
<input
337-
disabled={busy}
338-
value={options.buildCmd}
339-
onChange={(e) => setOption("buildCmd", e.target.value)}
340-
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
341-
placeholder="npm run build"
342-
/>
343-
</label>
404+
{/* Build command: only show if build stage enabled */}
405+
{stages.includes("build") && (
406+
<label className="grid gap-1">
407+
<span className="text-sm font-medium text-slate-800">
408+
{template === "node_app"
409+
? "Build command (npm)"
410+
: "Build command"}
411+
</span>
412+
<input
413+
disabled={busy}
414+
value={options.buildCmd}
415+
onChange={(e) => setOption("buildCmd", e.target.value)}
416+
className="rounded-md border border-white/25 px-3 py-2 text-sm font-mono text-white bg-white/10 placeholder-white/60 disabled:bg-white/5 disabled:text-slate-400"
417+
placeholder={
418+
template === "node_app"
419+
? "npm run build"
420+
: ""
421+
}
422+
/>
423+
</label>
424+
)}
344425
</div>
345426

346-
{provider === "aws" && (
427+
{provider === "aws" && stages.includes("deploy") && (
347428
<>
348429
<label className="grid gap-1">
349430
<span className="text-sm font-medium">AWS Role (OIDC)</span>

client/src/store/usePipelineStore.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,36 @@ type PipelineActions = {
6666
setResultYaml(yaml: string): void;
6767
};
6868

69+
const TEMPLATE_DEFAULT_OPTIONS: Record<string, PipelineState["options"]> = {
70+
node_app: {
71+
nodeVersion: "20",
72+
installCmd: "npm ci",
73+
testCmd: "npm test",
74+
buildCmd: "npm run build",
75+
awsSessionName: "autodeploy",
76+
awsRegion: "us-east-1",
77+
gcpServiceAccountEmail: "",
78+
},
79+
python_app: {
80+
nodeVersion: "",
81+
installCmd: "pip install -r requirements.txt",
82+
testCmd: "pytest",
83+
buildCmd: "",
84+
awsSessionName: "autodeploy",
85+
awsRegion: "us-east-1",
86+
gcpServiceAccountEmail: "",
87+
},
88+
container_service: {
89+
nodeVersion: "",
90+
installCmd: "",
91+
testCmd: "",
92+
buildCmd: "",
93+
awsSessionName: "autodeploy",
94+
awsRegion: "us-east-1",
95+
gcpServiceAccountEmail: "",
96+
},
97+
};
98+
6999
const initial: PipelineState = {
70100
template: "node_app",
71101
stages: ["build", "test", "deploy"],
@@ -91,7 +121,25 @@ export const usePipelineStore = create<PipelineState & PipelineActions>()(
91121
(set, get) => ({
92122
...initial,
93123

94-
setTemplate: (t) => set({ template: t }),
124+
setTemplate: (t) => {
125+
const current = get();
126+
const preservedProviderFields = {
127+
awsRoleArn: current.options.awsRoleArn,
128+
awsSessionName: current.options.awsSessionName,
129+
awsRegion: current.options.awsRegion,
130+
gcpServiceAccountEmail: current.options.gcpServiceAccountEmail,
131+
};
132+
133+
const nextDefaults = TEMPLATE_DEFAULT_OPTIONS[t] ?? TEMPLATE_DEFAULT_OPTIONS["node_app"];
134+
135+
set({
136+
template: t,
137+
options: {
138+
...nextDefaults,
139+
...preservedProviderFields,
140+
},
141+
});
142+
},
95143
setProvider: (p) => set({ provider: p }),
96144
setProvider: (p) => set({ provider: p }),
97145

0 commit comments

Comments
 (0)