@intentius/chant-lexicon-github 0.0.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.
- package/dist/integrity.json +31 -0
- package/dist/manifest.json +15 -0
- package/dist/meta.json +135 -0
- package/dist/rules/deprecated-action-version.ts +49 -0
- package/dist/rules/detect-secrets.ts +53 -0
- package/dist/rules/extract-inline-structs.ts +62 -0
- package/dist/rules/file-job-limit.ts +49 -0
- package/dist/rules/gha006.ts +58 -0
- package/dist/rules/gha009.ts +42 -0
- package/dist/rules/gha011.ts +40 -0
- package/dist/rules/gha017.ts +32 -0
- package/dist/rules/gha018.ts +40 -0
- package/dist/rules/gha019.ts +72 -0
- package/dist/rules/job-timeout.ts +59 -0
- package/dist/rules/missing-recommended-inputs.ts +61 -0
- package/dist/rules/no-hardcoded-secrets.ts +46 -0
- package/dist/rules/no-raw-expressions.ts +51 -0
- package/dist/rules/suggest-cache.ts +71 -0
- package/dist/rules/use-condition-builders.ts +45 -0
- package/dist/rules/use-matrix-builder.ts +44 -0
- package/dist/rules/use-typed-actions.ts +47 -0
- package/dist/rules/validate-concurrency.ts +66 -0
- package/dist/rules/yaml-helpers.ts +129 -0
- package/dist/skills/chant-github.md +29 -0
- package/dist/skills/github-actions-patterns.md +93 -0
- package/dist/types/index.d.ts +358 -0
- package/package.json +33 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1138 -0
- package/src/codegen/generate-cli.ts +36 -0
- package/src/codegen/generate-lexicon.ts +58 -0
- package/src/codegen/generate-typescript.ts +149 -0
- package/src/codegen/generate.ts +141 -0
- package/src/codegen/naming.ts +57 -0
- package/src/codegen/package.ts +65 -0
- package/src/codegen/parse.ts +700 -0
- package/src/codegen/patches.ts +46 -0
- package/src/composites/cache.ts +25 -0
- package/src/composites/checkout.ts +31 -0
- package/src/composites/composites.test.ts +675 -0
- package/src/composites/deploy-environment.ts +77 -0
- package/src/composites/docker-build.ts +120 -0
- package/src/composites/download-artifact.ts +24 -0
- package/src/composites/go-ci.ts +91 -0
- package/src/composites/index.ts +26 -0
- package/src/composites/node-ci.ts +71 -0
- package/src/composites/node-pipeline.ts +151 -0
- package/src/composites/python-ci.ts +92 -0
- package/src/composites/setup-go.ts +24 -0
- package/src/composites/setup-node.ts +26 -0
- package/src/composites/setup-python.ts +24 -0
- package/src/composites/upload-artifact.ts +27 -0
- package/src/coverage.ts +49 -0
- package/src/expression.test.ts +147 -0
- package/src/expression.ts +214 -0
- package/src/generated/index.d.ts +358 -0
- package/src/generated/index.ts +29 -0
- package/src/generated/lexicon-github.json +135 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +110 -0
- package/src/import/generator.ts +119 -0
- package/src/import/parser.test.ts +98 -0
- package/src/import/parser.ts +73 -0
- package/src/index.ts +53 -0
- package/src/lint/post-synth/gha006.ts +58 -0
- package/src/lint/post-synth/gha009.ts +42 -0
- package/src/lint/post-synth/gha011.ts +40 -0
- package/src/lint/post-synth/gha017.ts +32 -0
- package/src/lint/post-synth/gha018.ts +40 -0
- package/src/lint/post-synth/gha019.ts +72 -0
- package/src/lint/post-synth/post-synth.test.ts +318 -0
- package/src/lint/post-synth/yaml-helpers.ts +129 -0
- package/src/lint/rules/data/deprecated-versions.ts +13 -0
- package/src/lint/rules/data/known-actions.ts +13 -0
- package/src/lint/rules/data/recommended-inputs.ts +10 -0
- package/src/lint/rules/data/secret-patterns.ts +31 -0
- package/src/lint/rules/deprecated-action-version.ts +49 -0
- package/src/lint/rules/detect-secrets.ts +53 -0
- package/src/lint/rules/extract-inline-structs.ts +62 -0
- package/src/lint/rules/file-job-limit.ts +49 -0
- package/src/lint/rules/index.ts +17 -0
- package/src/lint/rules/job-timeout.ts +59 -0
- package/src/lint/rules/missing-recommended-inputs.ts +61 -0
- package/src/lint/rules/no-hardcoded-secrets.ts +46 -0
- package/src/lint/rules/no-raw-expressions.ts +51 -0
- package/src/lint/rules/rules.test.ts +365 -0
- package/src/lint/rules/suggest-cache.ts +71 -0
- package/src/lint/rules/use-condition-builders.ts +45 -0
- package/src/lint/rules/use-matrix-builder.ts +44 -0
- package/src/lint/rules/use-typed-actions.ts +47 -0
- package/src/lint/rules/validate-concurrency.ts +66 -0
- package/src/lsp/completions.test.ts +9 -0
- package/src/lsp/completions.ts +20 -0
- package/src/lsp/hover.test.ts +9 -0
- package/src/lsp/hover.ts +38 -0
- package/src/package-cli.ts +42 -0
- package/src/plugin.test.ts +128 -0
- package/src/plugin.ts +408 -0
- package/src/serializer.test.ts +270 -0
- package/src/serializer.ts +383 -0
- package/src/skills/github-actions-patterns.md +93 -0
- package/src/spec/fetch.ts +55 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.test.ts +12 -0
- package/src/validate.ts +32 -0
- package/src/variables.ts +44 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions lexicon plugin.
|
|
3
|
+
*
|
|
4
|
+
* Provides serializer, template detection, and code generation
|
|
5
|
+
* for GitHub Actions workflows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
import type { LexiconPlugin, IntrinsicDef, SkillDefinition, InitTemplateSet } from "@intentius/chant/lexicon";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
12
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
13
|
+
import { githubSerializer } from "./serializer";
|
|
14
|
+
|
|
15
|
+
export const githubPlugin: LexiconPlugin = {
|
|
16
|
+
name: "github",
|
|
17
|
+
serializer: githubSerializer,
|
|
18
|
+
|
|
19
|
+
lintRules(): LintRule[] {
|
|
20
|
+
const { useTypedActionsRule } = require("./lint/rules/use-typed-actions");
|
|
21
|
+
const { useConditionBuildersRule } = require("./lint/rules/use-condition-builders");
|
|
22
|
+
const { noHardcodedSecretsRule } = require("./lint/rules/no-hardcoded-secrets");
|
|
23
|
+
const { useMatrixBuilderRule } = require("./lint/rules/use-matrix-builder");
|
|
24
|
+
const { extractInlineStructsRule } = require("./lint/rules/extract-inline-structs");
|
|
25
|
+
const { fileJobLimitRule } = require("./lint/rules/file-job-limit");
|
|
26
|
+
const { noRawExpressionsRule } = require("./lint/rules/no-raw-expressions");
|
|
27
|
+
const { missingRecommendedInputsRule } = require("./lint/rules/missing-recommended-inputs");
|
|
28
|
+
const { deprecatedActionVersionRule } = require("./lint/rules/deprecated-action-version");
|
|
29
|
+
const { jobTimeoutRule } = require("./lint/rules/job-timeout");
|
|
30
|
+
const { suggestCacheRule } = require("./lint/rules/suggest-cache");
|
|
31
|
+
const { validateConcurrencyRule } = require("./lint/rules/validate-concurrency");
|
|
32
|
+
const { detectSecretsRule } = require("./lint/rules/detect-secrets");
|
|
33
|
+
return [
|
|
34
|
+
useTypedActionsRule,
|
|
35
|
+
useConditionBuildersRule,
|
|
36
|
+
noHardcodedSecretsRule,
|
|
37
|
+
useMatrixBuilderRule,
|
|
38
|
+
extractInlineStructsRule,
|
|
39
|
+
fileJobLimitRule,
|
|
40
|
+
noRawExpressionsRule,
|
|
41
|
+
missingRecommendedInputsRule,
|
|
42
|
+
deprecatedActionVersionRule,
|
|
43
|
+
jobTimeoutRule,
|
|
44
|
+
suggestCacheRule,
|
|
45
|
+
validateConcurrencyRule,
|
|
46
|
+
detectSecretsRule,
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
postSynthChecks(): PostSynthCheck[] {
|
|
51
|
+
const { gha006 } = require("./lint/post-synth/gha006");
|
|
52
|
+
const { gha009 } = require("./lint/post-synth/gha009");
|
|
53
|
+
const { gha011 } = require("./lint/post-synth/gha011");
|
|
54
|
+
const { gha017 } = require("./lint/post-synth/gha017");
|
|
55
|
+
const { gha018 } = require("./lint/post-synth/gha018");
|
|
56
|
+
const { gha019 } = require("./lint/post-synth/gha019");
|
|
57
|
+
return [gha006, gha009, gha011, gha017, gha018, gha019];
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
intrinsics(): IntrinsicDef[] {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
name: "expression",
|
|
64
|
+
description: "${{ }} expression wrapper for GitHub Actions contexts",
|
|
65
|
+
outputKey: "expression",
|
|
66
|
+
isTag: false,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
initTemplates(template?: string): InitTemplateSet {
|
|
72
|
+
if (template === "node-ci") {
|
|
73
|
+
return {
|
|
74
|
+
src: {
|
|
75
|
+
"pipeline.ts": `import { NodeCI } from "@intentius/chant-lexicon-github";
|
|
76
|
+
|
|
77
|
+
export const app = NodeCI({
|
|
78
|
+
nodeVersion: "22",
|
|
79
|
+
installCommand: "npm ci",
|
|
80
|
+
buildScript: "build",
|
|
81
|
+
testScript: "test",
|
|
82
|
+
});
|
|
83
|
+
`,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (template === "docker-build") {
|
|
89
|
+
return {
|
|
90
|
+
src: {
|
|
91
|
+
"pipeline.ts": `import { Job, Step, Workflow, PushTrigger, Checkout } from "@intentius/chant-lexicon-github";
|
|
92
|
+
|
|
93
|
+
export const push = new PushTrigger({ branches: ["main"] });
|
|
94
|
+
|
|
95
|
+
export const workflow = new Workflow({
|
|
96
|
+
name: "Docker Build",
|
|
97
|
+
on: { push: { branches: ["main"] } },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const build = new Job({
|
|
101
|
+
"runs-on": "ubuntu-latest",
|
|
102
|
+
steps: [
|
|
103
|
+
Checkout({}).step,
|
|
104
|
+
new Step({
|
|
105
|
+
name: "Build and push",
|
|
106
|
+
run: "docker build -t myapp .",
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
`,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Default template
|
|
116
|
+
return {
|
|
117
|
+
src: {
|
|
118
|
+
"pipeline.ts": `import { Workflow, Job, Step, Checkout, SetupNode } from "@intentius/chant-lexicon-github";
|
|
119
|
+
|
|
120
|
+
export const workflow = new Workflow({
|
|
121
|
+
name: "CI",
|
|
122
|
+
on: {
|
|
123
|
+
push: { branches: ["main"] },
|
|
124
|
+
pull_request: { branches: ["main"] },
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const build = new Job({
|
|
129
|
+
"runs-on": "ubuntu-latest",
|
|
130
|
+
steps: [
|
|
131
|
+
Checkout({}).step,
|
|
132
|
+
SetupNode({ nodeVersion: "22", cache: "npm" }).step,
|
|
133
|
+
new Step({ name: "Install", run: "npm ci" }),
|
|
134
|
+
new Step({ name: "Build", run: "npm run build" }),
|
|
135
|
+
new Step({ name: "Test", run: "npm test" }),
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
`,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
detectTemplate(data: unknown): boolean {
|
|
144
|
+
if (typeof data !== "object" || data === null) return false;
|
|
145
|
+
const obj = data as Record<string, unknown>;
|
|
146
|
+
|
|
147
|
+
// GitHub Actions workflows have `on:` + `jobs:` keys
|
|
148
|
+
if (obj.on !== undefined && obj.jobs !== undefined) return true;
|
|
149
|
+
|
|
150
|
+
// Check for job-like entries with runs-on
|
|
151
|
+
for (const value of Object.values(obj)) {
|
|
152
|
+
if (typeof value === "object" && value !== null) {
|
|
153
|
+
const entry = value as Record<string, unknown>;
|
|
154
|
+
if (entry["runs-on"] !== undefined || entry.steps !== undefined) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return false;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
completionProvider(ctx: import("@intentius/chant/lsp/types").CompletionContext) {
|
|
164
|
+
const { githubCompletions } = require("./lsp/completions");
|
|
165
|
+
return githubCompletions(ctx);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
hoverProvider(ctx: import("@intentius/chant/lsp/types").HoverContext) {
|
|
169
|
+
const { githubHover } = require("./lsp/hover");
|
|
170
|
+
return githubHover(ctx);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
templateParser() {
|
|
174
|
+
const { GitHubActionsParser } = require("./import/parser");
|
|
175
|
+
return new GitHubActionsParser();
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
templateGenerator() {
|
|
179
|
+
const { GitHubActionsGenerator } = require("./import/generator");
|
|
180
|
+
return new GitHubActionsGenerator();
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async generate(options?: { verbose?: boolean }): Promise<void> {
|
|
184
|
+
const { generate, writeGeneratedFiles } = await import("./codegen/generate");
|
|
185
|
+
const { dirname } = await import("path");
|
|
186
|
+
const { fileURLToPath } = await import("url");
|
|
187
|
+
|
|
188
|
+
const result = await generate({ verbose: options?.verbose ?? true });
|
|
189
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
190
|
+
writeGeneratedFiles(result, pkgDir);
|
|
191
|
+
|
|
192
|
+
console.error(
|
|
193
|
+
`Generated ${result.resources} entities, ${result.properties} property types, ${result.enums} enums`,
|
|
194
|
+
);
|
|
195
|
+
if (result.warnings.length > 0) {
|
|
196
|
+
console.error(`${result.warnings.length} warnings`);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
async validate(options?: { verbose?: boolean }): Promise<void> {
|
|
201
|
+
const { validate } = await import("./validate");
|
|
202
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
203
|
+
const result = await validate();
|
|
204
|
+
printValidationResult(result);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
208
|
+
const { analyzeGitHubCoverage } = await import("./coverage");
|
|
209
|
+
await analyzeGitHubCoverage({
|
|
210
|
+
verbose: options?.verbose,
|
|
211
|
+
minOverall: options?.minOverall,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
216
|
+
const { packageLexicon } = await import("./codegen/package");
|
|
217
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
218
|
+
const { join, dirname } = await import("path");
|
|
219
|
+
const { fileURLToPath } = await import("url");
|
|
220
|
+
|
|
221
|
+
const { spec, stats } = await packageLexicon({ verbose: options?.verbose, force: options?.force });
|
|
222
|
+
|
|
223
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
224
|
+
const distDir = join(pkgDir, "dist");
|
|
225
|
+
writeBundleSpec(spec, distDir);
|
|
226
|
+
|
|
227
|
+
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
mcpTools() {
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
name: "diff",
|
|
234
|
+
description: "Compare current build output against previous output for GitHub Actions",
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: "object" as const,
|
|
237
|
+
properties: {
|
|
238
|
+
path: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Path to the infrastructure project directory",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
245
|
+
const { diffCommand } = await import("@intentius/chant/cli/commands/diff");
|
|
246
|
+
const result = await diffCommand({
|
|
247
|
+
path: (params.path as string) ?? ".",
|
|
248
|
+
serializers: [githubSerializer],
|
|
249
|
+
});
|
|
250
|
+
return result;
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
mcpResources() {
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
uri: "resource-catalog",
|
|
260
|
+
name: "GitHub Actions Entity Catalog",
|
|
261
|
+
description: "JSON list of all supported GitHub Actions entity types",
|
|
262
|
+
mimeType: "application/json",
|
|
263
|
+
async handler(): Promise<string> {
|
|
264
|
+
const lexicon = require("./generated/lexicon-github.json") as Record<string, { resourceType: string; kind: string }>;
|
|
265
|
+
const entries = Object.entries(lexicon).map(([className, entry]) => ({
|
|
266
|
+
className,
|
|
267
|
+
resourceType: entry.resourceType,
|
|
268
|
+
kind: entry.kind,
|
|
269
|
+
}));
|
|
270
|
+
return JSON.stringify(entries);
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
uri: "examples/basic-ci",
|
|
275
|
+
name: "Basic CI Example",
|
|
276
|
+
description: "A basic GitHub Actions CI workflow with build and test",
|
|
277
|
+
mimeType: "text/typescript",
|
|
278
|
+
async handler(): Promise<string> {
|
|
279
|
+
return `import { Workflow, Job, Step, Checkout, SetupNode } from "@intentius/chant-lexicon-github";
|
|
280
|
+
|
|
281
|
+
export const workflow = new Workflow({
|
|
282
|
+
name: "CI",
|
|
283
|
+
on: {
|
|
284
|
+
push: { branches: ["main"] },
|
|
285
|
+
pull_request: { branches: ["main"] },
|
|
286
|
+
},
|
|
287
|
+
permissions: { contents: "read" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
export const build = new Job({
|
|
291
|
+
"runs-on": "ubuntu-latest",
|
|
292
|
+
steps: [
|
|
293
|
+
Checkout({}).step,
|
|
294
|
+
SetupNode({ nodeVersion: "22", cache: "npm" }).step,
|
|
295
|
+
new Step({ name: "Install", run: "npm ci" }),
|
|
296
|
+
new Step({ name: "Build", run: "npm run build" }),
|
|
297
|
+
new Step({ name: "Test", run: "npm test" }),
|
|
298
|
+
],
|
|
299
|
+
});
|
|
300
|
+
`;
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async docs(options?: { verbose?: boolean }): Promise<void> {
|
|
307
|
+
const { generateDocs } = await import("./codegen/docs");
|
|
308
|
+
await generateDocs(options);
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
skills(): SkillDefinition[] {
|
|
312
|
+
const skills: SkillDefinition[] = [
|
|
313
|
+
{
|
|
314
|
+
name: "chant-github",
|
|
315
|
+
description: "GitHub Actions workflow lifecycle — build, validate, deploy",
|
|
316
|
+
content: `---
|
|
317
|
+
skill: chant-github
|
|
318
|
+
description: Build, validate, and deploy GitHub Actions workflows from a chant project
|
|
319
|
+
user-invocable: true
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
# GitHub Actions Operational Playbook
|
|
323
|
+
|
|
324
|
+
## How chant and GitHub Actions relate
|
|
325
|
+
|
|
326
|
+
chant is a **synthesis-only** tool — it compiles TypeScript source files into \`.github/workflows/*.yml\` (YAML). chant does NOT call GitHub APIs.
|
|
327
|
+
|
|
328
|
+
- Use **chant** for: build, lint, diff (local YAML comparison)
|
|
329
|
+
- Use **git + GitHub** for: push, pull requests, workflow monitoring
|
|
330
|
+
|
|
331
|
+
## Build and validate
|
|
332
|
+
|
|
333
|
+
\`\`\`bash
|
|
334
|
+
chant build src/ --output .github/workflows/ci.yml
|
|
335
|
+
chant lint src/
|
|
336
|
+
\`\`\`
|
|
337
|
+
|
|
338
|
+
## Deploy
|
|
339
|
+
|
|
340
|
+
\`\`\`bash
|
|
341
|
+
git add .github/workflows/ci.yml
|
|
342
|
+
git commit -m "Update workflow"
|
|
343
|
+
git push
|
|
344
|
+
\`\`\`
|
|
345
|
+
`,
|
|
346
|
+
triggers: [
|
|
347
|
+
{ type: "file-pattern", value: "**/*.github.ts" },
|
|
348
|
+
{ type: "file-pattern", value: "**/.github/workflows/*.yml" },
|
|
349
|
+
{ type: "context", value: "github actions" },
|
|
350
|
+
{ type: "context", value: "workflow" },
|
|
351
|
+
],
|
|
352
|
+
parameters: [],
|
|
353
|
+
examples: [
|
|
354
|
+
{
|
|
355
|
+
title: "Basic CI workflow",
|
|
356
|
+
description: "Create a CI workflow with build and test",
|
|
357
|
+
input: "Create a CI workflow",
|
|
358
|
+
output: `new Workflow({ name: "CI", on: { push: { branches: ["main"] }, pull_request: { branches: ["main"] } } })`,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
},
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
// Load file-based skills
|
|
365
|
+
const { readFileSync } = require("fs");
|
|
366
|
+
const { join, dirname } = require("path");
|
|
367
|
+
const { fileURLToPath } = require("url");
|
|
368
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
369
|
+
|
|
370
|
+
const skillFiles = [
|
|
371
|
+
{
|
|
372
|
+
file: "github-actions-patterns.md",
|
|
373
|
+
name: "github-actions-patterns",
|
|
374
|
+
description: "GitHub Actions workflow patterns — triggers, jobs, matrix, caching, artifacts",
|
|
375
|
+
triggers: [
|
|
376
|
+
{ type: "context" as const, value: "github actions" },
|
|
377
|
+
{ type: "context" as const, value: "workflow" },
|
|
378
|
+
{ type: "context" as const, value: "matrix" },
|
|
379
|
+
{ type: "context" as const, value: "cache" },
|
|
380
|
+
],
|
|
381
|
+
parameters: [],
|
|
382
|
+
examples: [
|
|
383
|
+
{
|
|
384
|
+
title: "Matrix strategy",
|
|
385
|
+
input: "Set up a Node.js matrix build",
|
|
386
|
+
output: `new Strategy({ matrix: { "node-version": ["18", "20", "22"] } })`,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
for (const skill of skillFiles) {
|
|
393
|
+
try {
|
|
394
|
+
const content = readFileSync(join(dir, "skills", skill.file), "utf-8");
|
|
395
|
+
skills.push({
|
|
396
|
+
name: skill.name,
|
|
397
|
+
description: skill.description,
|
|
398
|
+
content,
|
|
399
|
+
triggers: skill.triggers,
|
|
400
|
+
parameters: skill.parameters,
|
|
401
|
+
examples: skill.examples,
|
|
402
|
+
});
|
|
403
|
+
} catch { /* skip missing skills */ }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return skills;
|
|
407
|
+
},
|
|
408
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { githubSerializer } from "./serializer";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
5
|
+
|
|
6
|
+
// ── Mock entities ──────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
class MockWorkflow implements Declarable {
|
|
9
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
10
|
+
readonly lexicon = "github";
|
|
11
|
+
readonly entityType = "GitHub::Actions::Workflow";
|
|
12
|
+
readonly kind = "resource" as const;
|
|
13
|
+
readonly props: Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
constructor(props: Record<string, unknown> = {}) {
|
|
16
|
+
this.props = props;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class MockJob implements Declarable {
|
|
21
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
22
|
+
readonly lexicon = "github";
|
|
23
|
+
readonly entityType = "GitHub::Actions::Job";
|
|
24
|
+
readonly kind = "resource" as const;
|
|
25
|
+
readonly props: Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
constructor(props: Record<string, unknown> = {}) {
|
|
28
|
+
this.props = props;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class MockStep implements Declarable {
|
|
33
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
34
|
+
readonly lexicon = "github";
|
|
35
|
+
readonly entityType = "GitHub::Actions::Step";
|
|
36
|
+
readonly kind = "property" as const;
|
|
37
|
+
readonly props: Record<string, unknown>;
|
|
38
|
+
|
|
39
|
+
constructor(props: Record<string, unknown> = {}) {
|
|
40
|
+
this.props = props;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class MockPushTrigger implements Declarable {
|
|
45
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
46
|
+
readonly lexicon = "github";
|
|
47
|
+
readonly entityType = "GitHub::Actions::PushTrigger";
|
|
48
|
+
readonly kind = "resource" as const;
|
|
49
|
+
readonly props: Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
constructor(props: Record<string, unknown> = {}) {
|
|
52
|
+
this.props = props;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Tests ──────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
describe("githubSerializer", () => {
|
|
59
|
+
test("has correct name", () => {
|
|
60
|
+
expect(githubSerializer.name).toBe("github");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("has correct rulePrefix", () => {
|
|
64
|
+
expect(githubSerializer.rulePrefix).toBe("GHA");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("githubSerializer.serialize", () => {
|
|
69
|
+
test("serializes empty entities", () => {
|
|
70
|
+
const entities = new Map<string, Declarable>();
|
|
71
|
+
const output = githubSerializer.serialize(entities);
|
|
72
|
+
expect(output).toBe("\n");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("serializes a workflow with name", () => {
|
|
76
|
+
const entities = new Map<string, Declarable>();
|
|
77
|
+
entities.set("workflow", new MockWorkflow({
|
|
78
|
+
name: "CI",
|
|
79
|
+
on: { push: { branches: ["main"] } },
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
83
|
+
expect(output).toContain("name: CI");
|
|
84
|
+
expect(output).toContain("on:");
|
|
85
|
+
expect(output).toContain("push:");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("serializes jobs with kebab-case names", () => {
|
|
89
|
+
const entities = new Map<string, Declarable>();
|
|
90
|
+
entities.set("workflow", new MockWorkflow({
|
|
91
|
+
name: "CI",
|
|
92
|
+
on: { push: { branches: ["main"] } },
|
|
93
|
+
}));
|
|
94
|
+
entities.set("buildAndTest", new MockJob({
|
|
95
|
+
"runs-on": "ubuntu-latest",
|
|
96
|
+
steps: [{ name: "Test", run: "npm test" }],
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
100
|
+
expect(output).toContain("build-and-test:");
|
|
101
|
+
expect(output).toContain("runs-on: ubuntu-latest");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("serializes triggers from workflow on: property", () => {
|
|
105
|
+
const entities = new Map<string, Declarable>();
|
|
106
|
+
entities.set("workflow", new MockWorkflow({
|
|
107
|
+
name: "CI",
|
|
108
|
+
on: {
|
|
109
|
+
push: { branches: ["main"] },
|
|
110
|
+
pullRequest: { branches: ["main"] },
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
115
|
+
expect(output).toContain("on:");
|
|
116
|
+
expect(output).toContain("push:");
|
|
117
|
+
expect(output).toContain("pull_request:");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("serializes permissions", () => {
|
|
121
|
+
const entities = new Map<string, Declarable>();
|
|
122
|
+
entities.set("workflow", new MockWorkflow({
|
|
123
|
+
name: "CI",
|
|
124
|
+
on: { push: null },
|
|
125
|
+
permissions: { contents: "read", pullRequests: "write" },
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
129
|
+
expect(output).toContain("permissions:");
|
|
130
|
+
expect(output).toContain("contents: read");
|
|
131
|
+
expect(output).toContain("pull-requests: write");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("serializes multiple jobs", () => {
|
|
135
|
+
const entities = new Map<string, Declarable>();
|
|
136
|
+
entities.set("workflow", new MockWorkflow({
|
|
137
|
+
name: "CI",
|
|
138
|
+
on: { push: { branches: ["main"] } },
|
|
139
|
+
}));
|
|
140
|
+
entities.set("build", new MockJob({ "runs-on": "ubuntu-latest" }));
|
|
141
|
+
entities.set("test", new MockJob({ "runs-on": "ubuntu-latest" }));
|
|
142
|
+
entities.set("deploy", new MockJob({ "runs-on": "ubuntu-latest" }));
|
|
143
|
+
|
|
144
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
145
|
+
expect(output).toContain("jobs:");
|
|
146
|
+
expect(output).toContain("build:");
|
|
147
|
+
expect(output).toContain("test:");
|
|
148
|
+
expect(output).toContain("deploy:");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("omits undefined/null values", () => {
|
|
152
|
+
const entities = new Map<string, Declarable>();
|
|
153
|
+
entities.set("workflow", new MockWorkflow({
|
|
154
|
+
name: "CI",
|
|
155
|
+
on: { push: { branches: ["main"] } },
|
|
156
|
+
}));
|
|
157
|
+
entities.set("job", new MockJob({
|
|
158
|
+
"runs-on": "ubuntu-latest",
|
|
159
|
+
timeoutMinutes: undefined,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
163
|
+
expect(output).not.toContain("timeout-minutes:");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("converts camelCase job props to kebab-case", () => {
|
|
167
|
+
const entities = new Map<string, Declarable>();
|
|
168
|
+
entities.set("workflow", new MockWorkflow({
|
|
169
|
+
name: "CI",
|
|
170
|
+
on: { push: null },
|
|
171
|
+
}));
|
|
172
|
+
entities.set("job", new MockJob({
|
|
173
|
+
"runs-on": "ubuntu-latest",
|
|
174
|
+
timeoutMinutes: 30,
|
|
175
|
+
continueOnError: true,
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
179
|
+
expect(output).toContain("timeout-minutes: 30");
|
|
180
|
+
expect(output).toContain("continue-on-error: true");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("serializes trigger entities", () => {
|
|
184
|
+
const entities = new Map<string, Declarable>();
|
|
185
|
+
entities.set("workflow", new MockWorkflow({
|
|
186
|
+
name: "CI",
|
|
187
|
+
}));
|
|
188
|
+
entities.set("push", new MockPushTrigger({
|
|
189
|
+
branches: ["main", "develop"],
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
193
|
+
expect(output).toContain("on:");
|
|
194
|
+
expect(output).toContain("push:");
|
|
195
|
+
expect(output).toContain("- main");
|
|
196
|
+
expect(output).toContain("- develop");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("multi-workflow output", () => {
|
|
201
|
+
test("produces SerializerResult with files", () => {
|
|
202
|
+
const entities = new Map<string, Declarable>();
|
|
203
|
+
entities.set("ci", new MockWorkflow({
|
|
204
|
+
name: "CI",
|
|
205
|
+
on: { push: { branches: ["main"] } },
|
|
206
|
+
}));
|
|
207
|
+
entities.set("deploy", new MockWorkflow({
|
|
208
|
+
name: "Deploy",
|
|
209
|
+
on: { push: { branches: ["main"] } },
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
const output = githubSerializer.serialize(entities);
|
|
213
|
+
expect(typeof output).toBe("object");
|
|
214
|
+
const result = output as { primary: string; files: Record<string, string> };
|
|
215
|
+
expect(result.files).toBeDefined();
|
|
216
|
+
expect(Object.keys(result.files).length).toBe(2);
|
|
217
|
+
expect(result.primary).toContain("name: CI");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("expression serialization", () => {
|
|
222
|
+
test("serializes intrinsic expressions to ${{ }} strings", () => {
|
|
223
|
+
const mockExpr = {
|
|
224
|
+
[INTRINSIC_MARKER]: true,
|
|
225
|
+
toYAML: () => "${{ github.ref }}",
|
|
226
|
+
toJSON: () => "${{ github.ref }}",
|
|
227
|
+
toString: () => "${{ github.ref }}",
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const entities = new Map<string, Declarable>();
|
|
231
|
+
entities.set("workflow", new MockWorkflow({
|
|
232
|
+
name: "CI",
|
|
233
|
+
on: { push: null },
|
|
234
|
+
}));
|
|
235
|
+
entities.set("job", new MockJob({
|
|
236
|
+
"runs-on": "ubuntu-latest",
|
|
237
|
+
if: mockExpr,
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
241
|
+
expect(output).toContain("${{ github.ref }}");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("YAML key ordering", () => {
|
|
246
|
+
test("emits keys in canonical order", () => {
|
|
247
|
+
const entities = new Map<string, Declarable>();
|
|
248
|
+
entities.set("workflow", new MockWorkflow({
|
|
249
|
+
name: "CI",
|
|
250
|
+
on: { push: null },
|
|
251
|
+
permissions: { contents: "read" },
|
|
252
|
+
env: { NODE_ENV: "production" },
|
|
253
|
+
}));
|
|
254
|
+
entities.set("job", new MockJob({
|
|
255
|
+
"runs-on": "ubuntu-latest",
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
const output = githubSerializer.serialize(entities) as string;
|
|
259
|
+
const nameIdx = output.indexOf("name:");
|
|
260
|
+
const onIdx = output.indexOf("on:");
|
|
261
|
+
const permIdx = output.indexOf("permissions:");
|
|
262
|
+
const envIdx = output.indexOf("env:");
|
|
263
|
+
const jobsIdx = output.indexOf("jobs:");
|
|
264
|
+
|
|
265
|
+
expect(nameIdx).toBeLessThan(onIdx);
|
|
266
|
+
expect(onIdx).toBeLessThan(permIdx);
|
|
267
|
+
expect(permIdx).toBeLessThan(envIdx);
|
|
268
|
+
expect(envIdx).toBeLessThan(jobsIdx);
|
|
269
|
+
});
|
|
270
|
+
});
|