Skip to content

Commit 75edc28

Browse files
committed
Fix YAML importer to preserve shell control structures as template literals
1 parent 151d1fb commit 75edc28

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@noxify/gitlab-ci-builder": patch
3+
---
4+
5+
Fix script parser to preserve shell control structures
6+
7+
The YAML importer now correctly detects and preserves shell control structures (if/then/else/fi, for/do/done, while/do/done, until/do/done, case/esac) as template literals instead of splitting them into separate array elements.
8+
9+
Previously, multi-line scripts with control structures were incorrectly split:
10+
11+
```typescript
12+
// Before (incorrect)
13+
script: ['if [ "$VAR" = "true" ]; then', 'echo "yes"', "else", 'echo "no"', "fi"]
14+
```
15+
16+
Now they are preserved as cohesive blocks:
17+
18+
```typescript
19+
// After (correct)
20+
script: [
21+
`if [ "$VAR" = "true" ]; then
22+
echo "yes"
23+
else
24+
echo "no"
25+
fi
26+
`,
27+
]
28+
```
29+
30+
This ensures shell scripts with control flow are generated correctly and maintain their intended structure.

src/import.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,19 @@ function formatScriptValue(value: unknown): string {
213213
/(?<!<)<(?!<)/, // Redirect input (but not <<) - negative lookbehind and lookahead
214214
]
215215

216+
// Check for shell control structures (if/then/else/fi, case/esac, for/do/done, while/do/done)
217+
const shellControlStructures = [
218+
/\bif\b.*\bthen\b/s, // if-then
219+
/\bcase\b.*\besac\b/s, // case-esac
220+
/\bfor\b.*\bdo\b/s, // for-do
221+
/\bwhile\b.*\bdo\b/s, // while-do
222+
/\buntil\b.*\bdo\b/s, // until-do
223+
]
224+
216225
const hasShellOperators = shellOperatorPatterns.some((pattern) => pattern.test(value))
226+
const hasControlStructure = shellControlStructures.some((pattern) => pattern.test(value))
217227

218-
if (hasShellOperators) {
228+
if (hasShellOperators || hasControlStructure) {
219229
// Keep as template literal to preserve exact formatting
220230
// Escape backticks and ${} in the string
221231
const escaped = value.replace(/`/g, "\\`").replace(/\$\{/g, "\\${")
@@ -311,8 +321,16 @@ function formatValue(value: unknown, indentLevel: number, addExtraIndent = false
311321
/&>/, // Redirect both
312322
/(?<!<)<(?!<)/, // Redirect input (but not <<)
313323
]
324+
const shellControlStructures = [
325+
/\bif\b.*\bthen\b/s, // if-then
326+
/\bcase\b.*\besac\b/s, // case-esac
327+
/\bfor\b.*\bdo\b/s, // for-do
328+
/\bwhile\b.*\bdo\b/s, // while-do
329+
/\buntil\b.*\bdo\b/s, // until-do
330+
]
314331
const hasShellOperators = shellOperatorPatterns.some((p) => p.test(item))
315-
if (!hasShellOperators) {
332+
const hasControlStructure = shellControlStructures.some((p) => p.test(item))
333+
if (!hasShellOperators && !hasControlStructure) {
316334
const linesSplit = item
317335
.split("\n")
318336
.map((l) => l.trim())

tests/import.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,70 @@ build:
274274
expect(ts).toContain('"npm run build"')
275275
expect(ts).toContain('echo \\"Done\\"')
276276
})
277+
278+
it("should preserve shell control structures (if/then/else/fi)", () => {
279+
const yaml = `
280+
deploy:
281+
script:
282+
- |
283+
if [ "$MANUAL_PROD_DEPLOYMENT" = "true" ]; then
284+
echo "🚨 MANUAL PRODUCTION DEPLOYMENT TRIGGERED 🚨"
285+
else
286+
echo "📦 Automated production deployment via changeset release"
287+
fi
288+
`
289+
290+
const ts = fromYaml(yaml)
291+
292+
expect(ts).toContain('config.job("deploy",')
293+
// Should be preserved as a template literal, not split into array
294+
expect(ts).toContain("if [")
295+
expect(ts).toContain("then")
296+
expect(ts).toContain("else")
297+
expect(ts).toContain("fi")
298+
// Should be in a single script item (template literal in array)
299+
expect(ts).toMatch(/script: \[`[\s\S]*if \[[\s\S]*then[\s\S]*else[\s\S]*fi[\s\S]*`\]/)
300+
})
301+
302+
it("should preserve shell for loops", () => {
303+
const yaml = `
304+
job:
305+
script:
306+
- |
307+
for i in 1 2 3; do
308+
echo "Item $i"
309+
done
310+
`
311+
312+
const ts = fromYaml(yaml)
313+
314+
expect(ts).toContain("for i in")
315+
expect(ts).toContain("do")
316+
expect(ts).toContain("done")
317+
expect(ts).toMatch(/script: \[`[\s\S]*for[\s\S]*do[\s\S]*done[\s\S]*`\]/)
318+
})
319+
320+
it("should preserve shell case statements", () => {
321+
const yaml = `
322+
job:
323+
script:
324+
- |
325+
case $ENV in
326+
prod)
327+
echo "Production"
328+
;;
329+
dev)
330+
echo "Development"
331+
;;
332+
esac
333+
`
334+
335+
const ts = fromYaml(yaml)
336+
337+
expect(ts).toContain("case")
338+
expect(ts).toContain("esac")
339+
expect(ts).toMatch(/script: \[`[\s\S]*case[\s\S]*esac[\s\S]*`\]/)
340+
})
277341
})
278342

279343
describe("importYamlFile()", () => {

0 commit comments

Comments
 (0)