Skip to content

Commit a04d3ff

Browse files
committed
feat: add spec.inputs support, improve test infrastructure with 24 GitLab template tests, and fix type safety for rules array access
1 parent c75bd58 commit a04d3ff

File tree

75 files changed

+3029
-154
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+3029
-154
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@noxify/gitlab-ci-builder": minor
3+
---
4+
5+
Add comprehensive test infrastructure for GitLab CI templates with local YAML storage and improve type safety for rules array access
6+
7+
- Add test helper utilities for GitLab template round-trip testing
8+
- Add 22 official GitLab CI templates as local test fixtures
9+
- Add integration tests for language templates (19 tests)
10+
- Add integration tests for infrastructure templates (3 tests)
11+
- Add integration tests for browser performance artifacts (2 tests)
12+
- Add support for `spec` with `inputs` (GitLab CI/CD components)
13+
- Add integration tests for multi-document YAML with OpenTofu component
14+
- Add interpolation support for schema fields that can contain GitLab CI/CD variables
15+
- Fix TypeScript type errors in test assertions for rules array access by adding proper type guards
16+
- Remove 'pages' from reserved job names (it's a valid GitLab Pages job name)
17+
- Reorganize tests into unit and integration directories

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ dist/
3535

3636
# turbo
3737
.turbo
38+
39+
**/.generated/

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
"lint:fix": "eslint . --fix",
4040
"test": "vitest --run",
4141
"test:coverage": "vitest --coverage",
42+
"test:integration": "vitest --run --project integration",
4243
"test:ui": "vitest --ui",
44+
"test:unit": "vitest --run --project unit",
4345
"test:watch": "vitest",
4446
"typecheck": "tsc --noEmit"
4547
},

src/builder/ConfigBuilder.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,36 @@ import type {
99
JobDefinitionNormalized,
1010
JobDefinitionOutput,
1111
JobOptions,
12+
Spec,
1213
ValidationError,
1314
Variables,
1415
Workflow,
1516
} from "../schema"
1617
import { mergeJobDefinitions } from "../merge"
1718
import { PipelineState } from "../model"
1819
import { resolveExtends } from "../resolution"
19-
import { DefaultsSchema, IncludeSchema, JobDefinitionParseSchema, WorkflowSchema } from "../schema"
20+
import {
21+
DefaultsSchema,
22+
IncludeSchema,
23+
JobDefinitionParseSchema,
24+
SpecSchema,
25+
WorkflowSchema,
26+
} from "../schema"
2027
import { serializeToYaml } from "../serializer"
2128

29+
/**
30+
* Reserved top-level keywords that cannot be used as job names
31+
* @see https://docs.gitlab.com/ee/ci/yaml/#keywords
32+
*/
33+
const RESERVED_JOB_NAMES = new Set([
34+
"default",
35+
"include",
36+
"stages",
37+
"variables",
38+
"workflow",
39+
"spec",
40+
])
41+
2242
/**
2343
* Macro args type
2444
*/
@@ -175,6 +195,20 @@ export class ConfigBuilder {
175195
return this
176196
}
177197

198+
/**
199+
* Set spec configuration (pipeline inputs for CI/CD components)
200+
* @see https://docs.gitlab.com/ee/ci/yaml/#spec
201+
*/
202+
public spec(spec: Spec) {
203+
const validated = SpecSchema.parse(spec)
204+
this.state.setSpec(validated)
205+
return this
206+
}
207+
208+
/**
209+
* Define a template (hidden job starting with dot)
210+
}
211+
178212
/**
179213
* Define a template (hidden job starting with dot)
180214
*
@@ -213,6 +247,13 @@ export class ConfigBuilder {
213247
* Define a job or template
214248
*/
215249
public job(name: string, definition: JobDefinitionInput, options: JobOptions = {}) {
250+
// Validate job name is not a reserved keyword
251+
if (RESERVED_JOB_NAMES.has(name)) {
252+
throw new Error(
253+
`Job name "${name}" is a reserved keyword and cannot be used as a job name. Reserved keywords: ${[...RESERVED_JOB_NAMES].join(", ")}`,
254+
)
255+
}
256+
216257
// Parse and normalize (extends is automatically normalized to array)
217258
const normalized = JobDefinitionParseSchema.parse(definition)
218259

@@ -360,6 +401,7 @@ export class ConfigBuilder {
360401

361402
// Build final output
362403
const pipeline: PipelineOutput = {
404+
spec: plain.spec,
363405
stages: plain.stages,
364406
workflow: plain.workflow,
365407
default: plain.default,

src/generate-types.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,35 +44,42 @@ async function generateTypes(): Promise<void> {
4444
]
4545

4646
const generatedTypes: string[] = [
47+
"/* eslint-disable @typescript-eslint/no-redundant-type-constituents */",
4748
"// Generated types from Zod schemas",
4849
"// Do not edit manually - run 'pnpm generate:types' to regenerate",
4950
"",
5051
]
5152

5253
for (const { name, schema } of schemas) {
53-
// Convert Zod schema to JSON Schema with inline expansion
54-
const jsonSchema = toJSONSchema(schema, {
55-
reused: "inline",
56-
unrepresentable: "any",
57-
})
54+
try {
55+
// Convert Zod schema to JSON Schema with inline expansion
56+
const jsonSchema = toJSONSchema(schema, {
57+
reused: "inline",
58+
unrepresentable: "any",
59+
})
5860

59-
// Convert JSON Schema to TypeScript
60-
let tsType = await compile(jsonSchema as typeof JSONSchema, name, {
61-
bannerComment: "",
62-
style: {
63-
printWidth: 100,
64-
semi: false,
65-
singleQuote: false,
66-
tabWidth: 2,
67-
},
68-
})
61+
// Convert JSON Schema to TypeScript
62+
let tsType = await compile(jsonSchema as typeof JSONSchema, name, {
63+
bannerComment: "",
64+
style: {
65+
printWidth: 100,
66+
semi: false,
67+
singleQuote: false,
68+
tabWidth: 2,
69+
},
70+
})
6971

70-
// Post-process: Only replace empty object types
71-
// Let ESLint fix the index signatures automatically
72-
tsType = tsType.replace(/\| \{\}/g, "| Record<string, unknown>")
73-
tsType = tsType.replace(/\{\}\[\]/g, "Record<string, unknown>[]")
72+
// Post-process: Only replace empty object types
73+
// Let ESLint fix the index signatures automatically
74+
tsType = tsType.replace(/\| \{\}/g, "| Record<string, unknown>")
75+
tsType = tsType.replace(/\{\}\[\]/g, "Record<string, unknown>[]")
7476

75-
generatedTypes.push(tsType)
77+
generatedTypes.push(tsType)
78+
} catch (error) {
79+
// eslint-disable-next-line no-console
80+
console.error(`❌ Failed to generate type for ${name}:`, error)
81+
throw error
82+
}
7683
}
7784

7885
// Write to file

0 commit comments

Comments
 (0)