@intentius/chant-lexicon-github 0.124.31 → 0.124.33

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.
Files changed (157) hide show
  1. package/dist/action-pins.js +53 -0
  2. package/dist/codegen/docs-cli.js +2 -0
  3. package/dist/codegen/docs.js +142 -0
  4. package/dist/codegen/generate-cli.js +26 -0
  5. package/dist/codegen/generate-lexicon.js +36 -0
  6. package/dist/codegen/generate-typescript.js +108 -0
  7. package/dist/codegen/generate.js +111 -0
  8. package/dist/codegen/naming.js +49 -0
  9. package/dist/codegen/package.js +40 -0
  10. package/dist/codegen/parse.js +595 -0
  11. package/dist/codegen/patches.js +36 -0
  12. package/dist/components/ci-jobs.d.ts +7 -0
  13. package/dist/components/ci-jobs.d.ts.map +1 -1
  14. package/dist/components/ci-jobs.js +98 -0
  15. package/dist/components/generate-op-pipeline.js +440 -0
  16. package/dist/components/generate-op-waves-pipeline.js +158 -0
  17. package/dist/components/generate-pipeline.d.ts.map +1 -1
  18. package/dist/components/generate-pipeline.js +481 -0
  19. package/dist/composites/cache.js +17 -0
  20. package/dist/composites/catalog.js +859 -0
  21. package/dist/composites/checkout.js +25 -0
  22. package/dist/composites/dependabot.js +43 -0
  23. package/dist/composites/deploy-environment.js +38 -0
  24. package/dist/composites/docker-build.js +70 -0
  25. package/dist/composites/download-artifact.js +18 -0
  26. package/dist/composites/go-ci.js +62 -0
  27. package/dist/composites/index.js +15 -0
  28. package/dist/composites/node-ci.js +44 -0
  29. package/dist/composites/node-pipeline.js +96 -0
  30. package/dist/composites/pr-plan-report.js +129 -0
  31. package/dist/composites/python-ci.js +60 -0
  32. package/dist/composites/setup-go.js +18 -0
  33. package/dist/composites/setup-node.js +21 -0
  34. package/dist/composites/setup-python.js +18 -0
  35. package/dist/composites/upload-artifact.js +19 -0
  36. package/dist/coverage.js +32 -0
  37. package/dist/expression.js +177 -0
  38. package/dist/generated/index.js +26 -0
  39. package/dist/generated/runtime.js +4 -0
  40. package/dist/graph-edges.js +174 -0
  41. package/dist/import/generator.js +108 -0
  42. package/dist/import/parser.js +69 -0
  43. package/dist/index.js +22 -0
  44. package/dist/integrity.json +2 -2
  45. package/dist/lint/audit-catalog.js +69 -0
  46. package/dist/lint/audit-lineage.js +168 -0
  47. package/dist/lint/post-synth/gha006.js +50 -0
  48. package/dist/lint/post-synth/gha009.js +35 -0
  49. package/dist/lint/post-synth/gha011.js +34 -0
  50. package/dist/lint/post-synth/gha013.js +40 -0
  51. package/dist/lint/post-synth/gha017.js +25 -0
  52. package/dist/lint/post-synth/gha018.js +33 -0
  53. package/dist/lint/post-synth/gha019.js +58 -0
  54. package/dist/lint/post-synth/gha021.js +42 -0
  55. package/dist/lint/post-synth/gha022.js +44 -0
  56. package/dist/lint/post-synth/gha023.js +37 -0
  57. package/dist/lint/post-synth/gha024.js +32 -0
  58. package/dist/lint/post-synth/gha025.js +33 -0
  59. package/dist/lint/post-synth/gha026.js +32 -0
  60. package/dist/lint/post-synth/gha027.js +46 -0
  61. package/dist/lint/post-synth/gha028.js +28 -0
  62. package/dist/lint/post-synth/gha029.js +56 -0
  63. package/dist/lint/post-synth/gha030.js +35 -0
  64. package/dist/lint/post-synth/gha031.js +75 -0
  65. package/dist/lint/post-synth/gha032.js +35 -0
  66. package/dist/lint/post-synth/gha033.js +39 -0
  67. package/dist/lint/post-synth/gha034.js +35 -0
  68. package/dist/lint/post-synth/gha035.js +50 -0
  69. package/dist/lint/post-synth/gha036.js +37 -0
  70. package/dist/lint/post-synth/gha037.js +41 -0
  71. package/dist/lint/post-synth/gha038.js +35 -0
  72. package/dist/lint/post-synth/gha039.js +34 -0
  73. package/dist/lint/post-synth/gha040.js +36 -0
  74. package/dist/lint/post-synth/gha041.js +32 -0
  75. package/dist/lint/post-synth/gha042.js +33 -0
  76. package/dist/lint/post-synth/gha043.js +35 -0
  77. package/dist/lint/post-synth/gha044.js +36 -0
  78. package/dist/lint/post-synth/gha045.js +33 -0
  79. package/dist/lint/post-synth/gha046.js +55 -0
  80. package/dist/lint/post-synth/gha047.js +44 -0
  81. package/dist/lint/post-synth/gha048.js +34 -0
  82. package/dist/lint/post-synth/gha049.js +36 -0
  83. package/dist/lint/post-synth/gha050.js +37 -0
  84. package/dist/lint/post-synth/gha051.js +38 -0
  85. package/dist/lint/post-synth/gha052.js +35 -0
  86. package/dist/lint/post-synth/gha053.js +34 -0
  87. package/dist/lint/post-synth/gha054.js +35 -0
  88. package/dist/lint/post-synth/gha055.js +44 -0
  89. package/dist/lint/post-synth/gha056.js +32 -0
  90. package/dist/lint/post-synth/gha057.js +35 -0
  91. package/dist/lint/post-synth/gha058.js +42 -0
  92. package/dist/lint/post-synth/gha059.js +119 -0
  93. package/dist/lint/post-synth/gha060.js +129 -0
  94. package/dist/lint/post-synth/gha061.js +78 -0
  95. package/dist/lint/post-synth/gha062.js +65 -0
  96. package/dist/lint/post-synth/gha063.js +56 -0
  97. package/dist/lint/post-synth/gha064.js +47 -0
  98. package/dist/lint/post-synth/gha065.js +125 -0
  99. package/dist/lint/post-synth/gha066.js +40 -0
  100. package/dist/lint/post-synth/gha067.js +75 -0
  101. package/dist/lint/post-synth/gha068.js +47 -0
  102. package/dist/lint/post-synth/gha069.js +99 -0
  103. package/dist/lint/post-synth/index.js +114 -0
  104. package/dist/lint/post-synth/yaml-helpers.js +465 -0
  105. package/dist/lint/rules/data/action-usage-policy.js +16 -0
  106. package/dist/lint/rules/data/advisory-feed.js +15 -0
  107. package/dist/lint/rules/data/deprecated-versions.js +14 -0
  108. package/dist/lint/rules/data/flagged-actions.js +18 -0
  109. package/dist/lint/rules/data/known-action-slugs.js +38 -0
  110. package/dist/lint/rules/data/known-actions.js +12 -0
  111. package/dist/lint/rules/data/preinstalled-tools.js +36 -0
  112. package/dist/lint/rules/data/recommended-inputs.js +9 -0
  113. package/dist/lint/rules/data/risky-features.js +22 -0
  114. package/dist/lint/rules/data/secret-patterns.js +30 -0
  115. package/dist/lint/rules/data/trusted-action-owners.js +12 -0
  116. package/dist/lint/rules/data/untrusted-contexts.js +44 -0
  117. package/dist/lint/rules/deprecated-action-version.js +44 -0
  118. package/dist/lint/rules/detect-secrets.js +45 -0
  119. package/dist/lint/rules/extract-inline-structs.js +55 -0
  120. package/dist/lint/rules/file-job-limit.js +43 -0
  121. package/dist/lint/rules/index.js +16 -0
  122. package/dist/lint/rules/job-timeout.js +53 -0
  123. package/dist/lint/rules/missing-recommended-inputs.js +51 -0
  124. package/dist/lint/rules/no-hardcoded-secrets.js +39 -0
  125. package/dist/lint/rules/no-raw-expressions.js +44 -0
  126. package/dist/lint/rules/suggest-cache.js +60 -0
  127. package/dist/lint/rules/use-condition-builders.js +37 -0
  128. package/dist/lint/rules/use-matrix-builder.js +36 -0
  129. package/dist/lint/rules/use-typed-actions.js +41 -0
  130. package/dist/lint/rules/validate-concurrency.js +59 -0
  131. package/dist/lsp/completions.d.ts.map +1 -1
  132. package/dist/lsp/completions.js +18 -0
  133. package/dist/lsp/hover.d.ts.map +1 -1
  134. package/dist/lsp/hover.js +33 -0
  135. package/dist/manifest.json +1 -1
  136. package/dist/mcp/context-tools.js +152 -0
  137. package/dist/package-cli.js +20 -0
  138. package/dist/plugin.js +344 -0
  139. package/dist/serializer.js +572 -0
  140. package/dist/spec/fetch.js +46 -0
  141. package/dist/validate-cli.js +16 -0
  142. package/dist/validate.js +38 -0
  143. package/dist/variables.js +41 -0
  144. package/package.json +6 -6
  145. package/src/codegen/package.ts +2 -2
  146. package/src/components/__fixtures__/pr-loop-build.github.golden.yml +6 -2
  147. package/src/components/__fixtures__/pr-loop-record.github.golden.yml +74 -0
  148. package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +178 -0
  149. package/src/components/__fixtures__/pr-loop.github.golden.yml +6 -2
  150. package/src/components/ci-jobs.ts +16 -2
  151. package/src/components/generate-op-pipeline.test.ts +11 -0
  152. package/src/components/generate-op-pipeline.ts +3 -3
  153. package/src/components/generate-pipeline.ts +74 -31
  154. package/src/components/generate-pr-pipeline.test.ts +99 -29
  155. package/src/components/generate-pr-waves-pipeline.test.ts +117 -0
  156. package/src/lsp/completions.ts +2 -1
  157. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,572 @@
1
+ /**
2
+ * GitHub Actions YAML serializer.
3
+ *
4
+ * Converts Chant declarables to .github/workflows/*.yml YAML output.
5
+ * Uses kebab-case keys for job properties and snake_case for trigger
6
+ * event names.
7
+ */
8
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
9
+ import { walkValue } from "@intentius/chant/serializer-walker";
10
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
11
+ import { emitYAML, emitYAMLEntry } from "@intentius/chant/yaml";
12
+ // ── Key conversion ────────────────────────────────────────────────
13
+ /**
14
+ * Convert camelCase property names to kebab-case for YAML output.
15
+ * Examples: timeoutMinutes → timeout-minutes, runsOn → runs-on
16
+ */
17
+ function toKebabCase(name) {
18
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
19
+ }
20
+ /**
21
+ * Convert camelCase trigger names to snake_case event names.
22
+ * Examples: pullRequest → pull_request, workflowDispatch → workflow_dispatch
23
+ */
24
+ function toSnakeCase(name) {
25
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
26
+ }
27
+ /**
28
+ * Map entity type names to YAML trigger event names.
29
+ */
30
+ const TRIGGER_TYPE_TO_EVENT = {
31
+ "GitHub::Actions::PushTrigger": "push",
32
+ "GitHub::Actions::PullRequestTrigger": "pull_request",
33
+ "GitHub::Actions::PullRequestTargetTrigger": "pull_request_target",
34
+ "GitHub::Actions::ScheduleTrigger": "schedule",
35
+ "GitHub::Actions::WorkflowDispatchTrigger": "workflow_dispatch",
36
+ "GitHub::Actions::WorkflowCallTrigger": "workflow_call",
37
+ "GitHub::Actions::WorkflowRunTrigger": "workflow_run",
38
+ "GitHub::Actions::RepositoryDispatchTrigger": "repository_dispatch",
39
+ };
40
+ /** Check if an entity type is a trigger. */
41
+ function isTriggerType(entityType) {
42
+ return entityType in TRIGGER_TYPE_TO_EVENT;
43
+ }
44
+ // ── Comments ──────────────────────────────────────────────────────
45
+ /**
46
+ * Entity types whose `comment` prop is YAML comment lines, not a key (#3667):
47
+ * the workflow's above `name:`, a job's above its key, a step's above its
48
+ * `- `. The prop never reaches the emitted mapping.
49
+ */
50
+ const COMMENTED_TYPES = new Set([
51
+ "GitHub::Actions::Workflow",
52
+ "GitHub::Actions::Job",
53
+ "GitHub::Actions::ReusableWorkflowCallJob",
54
+ "GitHub::Actions::Step",
55
+ ]);
56
+ /**
57
+ * The comment for each serialized job or step mapping, keyed by the object
58
+ * the emitter will see. Attached after key conversion, which builds new
59
+ * objects, and read by `emitJobs`.
60
+ */
61
+ const COMMENTS = new WeakMap();
62
+ /** The `comment` of a workflow, job or step entity, if it has one. */
63
+ function commentOf(value) {
64
+ if (!value || typeof value !== "object" || !("entityType" in value) || !("props" in value))
65
+ return undefined;
66
+ if (!COMMENTED_TYPES.has(value.entityType))
67
+ return undefined;
68
+ const comment = value.props?.comment;
69
+ return typeof comment === "string" && comment !== "" ? comment : undefined;
70
+ }
71
+ /** `props` without its `comment`, for an entity whose comment is not a key. */
72
+ function withoutComment(props) {
73
+ if (!props || typeof props !== "object" || !("comment" in props))
74
+ return props;
75
+ const { comment: _comment, ...rest } = props;
76
+ return rest;
77
+ }
78
+ /** A comment as `#` lines at `indent` (two spaces each), one per line of the string. */
79
+ function commentLines(comment, indent) {
80
+ const prefix = " ".repeat(indent);
81
+ return comment.split("\n").map((l) => `${prefix}#${l.trimEnd() ? ` ${l.trimEnd()}` : ""}`).join("\n");
82
+ }
83
+ /** Record a serialized job's comment and its steps' comments, from the job entity it came from. */
84
+ function attachComments(source, job) {
85
+ if (!job)
86
+ return;
87
+ const comment = commentOf(source);
88
+ if (comment)
89
+ COMMENTS.set(job, comment);
90
+ const rawSteps = source?.props?.steps;
91
+ if (!Array.isArray(rawSteps) || !Array.isArray(job.steps))
92
+ return;
93
+ rawSteps.forEach((raw, i) => {
94
+ const stepComment = commentOf(raw);
95
+ const step = job.steps[i];
96
+ if (stepComment && step && typeof step === "object")
97
+ COMMENTS.set(step, stepComment);
98
+ });
99
+ }
100
+ // ── Visitor ───────────────────────────────────────────────────────
101
+ function githubVisitor(entityNames) {
102
+ return {
103
+ attrRef: (name, _attr) => name,
104
+ resourceRef: (name) => toKebabCase(name),
105
+ propertyDeclarable: (entity, walk) => {
106
+ if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
107
+ return undefined;
108
+ }
109
+ const props = entity.props;
110
+ const commented = COMMENTED_TYPES.has(entity.entityType);
111
+ const result = {};
112
+ for (const [key, value] of Object.entries(props)) {
113
+ if (commented && key === "comment")
114
+ continue;
115
+ if (value !== undefined) {
116
+ result[key] = walk(value);
117
+ }
118
+ }
119
+ return Object.keys(result).length > 0 ? result : undefined;
120
+ },
121
+ };
122
+ }
123
+ // ── Intrinsic preprocessing ───────────────────────────────────────
124
+ function preprocessIntrinsics(value) {
125
+ if (value === null || value === undefined)
126
+ return value;
127
+ if (typeof value === "object" && INTRINSIC_MARKER in value) {
128
+ if ("toYAML" in value && typeof value.toYAML === "function") {
129
+ return value.toYAML();
130
+ }
131
+ }
132
+ // Leave Declarables untouched
133
+ if (typeof value === "object" && value !== null && "entityType" in value) {
134
+ return value;
135
+ }
136
+ if (Array.isArray(value)) {
137
+ return value.map(preprocessIntrinsics);
138
+ }
139
+ if (typeof value === "object") {
140
+ const result = {};
141
+ for (const [k, v] of Object.entries(value)) {
142
+ result[k] = preprocessIntrinsics(v);
143
+ }
144
+ return result;
145
+ }
146
+ return value;
147
+ }
148
+ function toYAMLValue(value, entityNames) {
149
+ const preprocessed = preprocessIntrinsics(value);
150
+ return walkValue(preprocessed, entityNames, githubVisitor(entityNames));
151
+ }
152
+ // ── Inline job serialization ──────────────────────────────────────
153
+ const JOB_ENTITY_TYPES = new Set([
154
+ "GitHub::Actions::Job",
155
+ "GitHub::Actions::ReusableWorkflowCallJob",
156
+ ]);
157
+ /**
158
+ * Serialize a value from Workflow.props.jobs into a YAML job object.
159
+ *
160
+ * When the value is a Job entity (resource Declarable), its props are inlined
161
+ * directly rather than emitted as a resource reference. This allows callers to
162
+ * pass `new Job({...})` directly inside `Workflow({ jobs: { name: new Job({...}) } })`.
163
+ *
164
+ * Plain objects are accepted too (JSON-style job definitions).
165
+ */
166
+ function serializeInlineJob(jobValue, entityNames) {
167
+ if (!jobValue || typeof jobValue !== "object")
168
+ return undefined;
169
+ const obj = jobValue;
170
+ if ("entityType" in obj && "props" in obj && JOB_ENTITY_TYPES.has(obj.entityType)) {
171
+ // Job entity: serialize its props inline (not as a resource reference)
172
+ const props = toYAMLValue(withoutComment(obj.props), entityNames);
173
+ const job = props && typeof props === "object"
174
+ ? convertKeys(props)
175
+ : undefined;
176
+ attachComments(obj, job);
177
+ return job;
178
+ }
179
+ // Plain object job definition (JSON-style)
180
+ return convertKeys(convertValueKeys(obj));
181
+ }
182
+ /**
183
+ * Build a jobs section from Workflow.props.jobs entries.
184
+ * Returns undefined when props.jobs is absent or empty.
185
+ */
186
+ function buildInlineJobsSection(props, entityNames) {
187
+ if (!props.jobs || typeof props.jobs !== "object" || Array.isArray(props.jobs))
188
+ return undefined;
189
+ const jobsSection = {};
190
+ for (const [jName, jobValue] of Object.entries(props.jobs)) {
191
+ const serialized = serializeInlineJob(jobValue, entityNames);
192
+ if (serialized)
193
+ jobsSection[toKebabCase(jName)] = serialized;
194
+ }
195
+ return Object.keys(jobsSection).length > 0 ? jobsSection : undefined;
196
+ }
197
+ /**
198
+ * Build a jobs section from standalone top-level Job entity exports.
199
+ * Returns undefined when there are no standalone jobs.
200
+ */
201
+ function buildStandaloneJobsSection(jobs, entityNames) {
202
+ if (jobs.length === 0)
203
+ return undefined;
204
+ const jobsSection = {};
205
+ for (const [name, job] of jobs) {
206
+ const jProps = toYAMLValue(withoutComment(isResourceDeclarable(job) ? job.props : undefined), entityNames);
207
+ if (!jProps)
208
+ continue;
209
+ const converted = convertKeys(jProps);
210
+ attachComments(job, converted);
211
+ jobsSection[toKebabCase(name)] = converted;
212
+ }
213
+ return Object.keys(jobsSection).length > 0 ? jobsSection : undefined;
214
+ }
215
+ // ── Key conversion for YAML output ────────────────────────────────
216
+ /**
217
+ * Map keys whose entries are user-named, not schema properties. `env` names
218
+ * environment variables (`KUBECONFIG_DATA` must stay `KUBECONFIG_DATA`, not
219
+ * become `kubeconfig_data`), `with` names an action's declared inputs,
220
+ * `outputs`/`secrets`/`variables` name user identifiers referenced elsewhere
221
+ * by exact spelling. Kebab-casing inside these silently renames them.
222
+ */
223
+ const VERBATIM_KEY_MAPS = new Set(["env", "with", "outputs", "secrets", "variables"]);
224
+ /**
225
+ * Convert a props object keys from camelCase to kebab-case for job/step properties.
226
+ */
227
+ function convertKeys(obj) {
228
+ const result = {};
229
+ for (const [key, value] of Object.entries(obj)) {
230
+ if (value === undefined || value === null)
231
+ continue;
232
+ // Zero and false are valid values, but undefined/null should be omitted
233
+ const yamlKey = toKebabCase(key);
234
+ result[yamlKey] = convertValueKeys(value, VERBATIM_KEY_MAPS.has(yamlKey));
235
+ }
236
+ return result;
237
+ }
238
+ function convertValueKeys(value, verbatim = false) {
239
+ if (value === null || value === undefined)
240
+ return value;
241
+ if (typeof value !== "object")
242
+ return value;
243
+ if (Array.isArray(value))
244
+ return value.map((v) => convertValueKeys(v, verbatim));
245
+ const obj = value;
246
+ const result = {};
247
+ for (const [key, v] of Object.entries(obj)) {
248
+ if (v === undefined || v === null)
249
+ continue;
250
+ const yamlKey = verbatim ? key : toKebabCase(key);
251
+ result[yamlKey] = convertValueKeys(v, verbatim || VERBATIM_KEY_MAPS.has(yamlKey));
252
+ }
253
+ return result;
254
+ }
255
+ // ── Serializer ────────────────────────────────────────────────────
256
+ /**
257
+ * GitHub Actions YAML serializer implementation.
258
+ */
259
+ export const githubSerializer = {
260
+ name: "github",
261
+ rulePrefix: "GHA",
262
+ serialize(entities, _outputs) {
263
+ const entityNames = new Map();
264
+ for (const [name, entity] of entities) {
265
+ entityNames.set(entity, name);
266
+ }
267
+ // Categorize entities
268
+ const workflows = [];
269
+ const jobs = [];
270
+ const triggers = [];
271
+ const dependabot = [];
272
+ const others = [];
273
+ for (const [name, entity] of entities) {
274
+ if (isPropertyDeclarable(entity))
275
+ continue;
276
+ const entityType = entity.entityType;
277
+ if (entityType === "GitHub::Actions::Workflow") {
278
+ workflows.push([name, entity]);
279
+ }
280
+ else if (entityType === "GitHub::Actions::Job" || entityType === "GitHub::Actions::ReusableWorkflowCallJob") {
281
+ jobs.push([name, entity]);
282
+ }
283
+ else if (isTriggerType(entityType)) {
284
+ triggers.push([name, entity]);
285
+ }
286
+ else if (entityType === "GitHub::Dependabot::Config") {
287
+ dependabot.push([name, entity]);
288
+ }
289
+ else {
290
+ others.push([name, entity]);
291
+ }
292
+ }
293
+ const workflowOut = workflows.length > 1
294
+ ? serializeMultiWorkflow(workflows, jobs, triggers, entities, entityNames)
295
+ : serializeSingleWorkflow(workflows, jobs, triggers, entities, entityNames);
296
+ // A DependabotConfig emits an additional `.github/dependabot.yml` file
297
+ // alongside (or instead of) any workflow output.
298
+ if (dependabot.length > 0) {
299
+ const [, dep] = dependabot[0];
300
+ const depProps = (isResourceDeclarable(dep) ? dep.props : undefined);
301
+ const dependabotYaml = emitDependabotYaml(depProps);
302
+ if (typeof workflowOut === "string") {
303
+ return { primary: workflowOut, files: { "dependabot.yml": dependabotYaml } };
304
+ }
305
+ return { ...workflowOut, files: { ...(workflowOut.files ?? {}), "dependabot.yml": dependabotYaml } };
306
+ }
307
+ return workflowOut;
308
+ },
309
+ };
310
+ /** Deep camelCase → kebab-case key conversion for dependabot props. */
311
+ function deepKebabKeys(value) {
312
+ if (Array.isArray(value))
313
+ return value.map(deepKebabKeys);
314
+ if (value !== null && typeof value === "object") {
315
+ const out = {};
316
+ for (const [k, v] of Object.entries(value)) {
317
+ out[toKebabCase(k)] = deepKebabKeys(v);
318
+ }
319
+ return out;
320
+ }
321
+ return value;
322
+ }
323
+ /** Serialize a DependabotConfig's props into `.github/dependabot.yml`. */
324
+ function emitDependabotYaml(props) {
325
+ const doc = deepKebabKeys(props);
326
+ return emitYAMLDocument(doc);
327
+ }
328
+ function serializeSingleWorkflow(workflows, jobs, triggers, _entities, entityNames) {
329
+ const doc = {};
330
+ // Workflow-level properties
331
+ if (workflows.length > 0) {
332
+ const [, wf] = workflows[0];
333
+ const props = toYAMLValue(isResourceDeclarable(wf) ? wf.props : undefined, entityNames);
334
+ if (props) {
335
+ if (props.name)
336
+ doc.name = props.name;
337
+ if (props["run-name"] || props.runName)
338
+ doc["run-name"] = props["run-name"] ?? props.runName;
339
+ if (props.permissions)
340
+ doc.permissions = convertValueKeys(props.permissions);
341
+ if (props.env)
342
+ doc.env = props.env;
343
+ if (props.concurrency)
344
+ doc.concurrency = convertValueKeys(props.concurrency);
345
+ if (props.defaults)
346
+ doc.defaults = convertValueKeys(props.defaults);
347
+ // Handle 'on' from workflow props
348
+ if (props.on) {
349
+ doc.on = convertTriggerProps(props.on);
350
+ }
351
+ }
352
+ }
353
+ // If triggers exist as separate entities, merge into 'on'
354
+ if (triggers.length > 0) {
355
+ const onSection = doc.on ?? {};
356
+ for (const [, trigger] of triggers) {
357
+ const entityType = trigger.entityType;
358
+ const eventName = TRIGGER_TYPE_TO_EVENT[entityType];
359
+ if (!eventName)
360
+ continue;
361
+ const props = toYAMLValue(isResourceDeclarable(trigger) ? trigger.props : undefined, entityNames);
362
+ if (props && Object.keys(props).length > 0) {
363
+ onSection[eventName] = convertValueKeys(props);
364
+ }
365
+ else {
366
+ onSection[eventName] = null;
367
+ }
368
+ }
369
+ doc.on = onSection;
370
+ }
371
+ // Jobs: prefer Workflow.props.jobs (raw) when present; fall back to standalone Job exports
372
+ let jobsSection;
373
+ if (workflows.length > 0) {
374
+ const rawProps = (isResourceDeclarable(workflows[0][1]) ? workflows[0][1].props : undefined);
375
+ jobsSection = buildInlineJobsSection(rawProps, entityNames);
376
+ }
377
+ if (!jobsSection)
378
+ jobsSection = buildStandaloneJobsSection(jobs, entityNames);
379
+ if (jobsSection)
380
+ doc.jobs = jobsSection;
381
+ return emitYAMLDocument(doc, workflows.length > 0 ? commentOf(workflows[0][1]) : undefined);
382
+ }
383
+ function serializeMultiWorkflow(workflows, jobs, triggers, _entities, entityNames) {
384
+ const files = {};
385
+ let primary = "";
386
+ for (let i = 0; i < workflows.length; i++) {
387
+ const [name, wf] = workflows[i];
388
+ const doc = {};
389
+ const props = toYAMLValue(isResourceDeclarable(wf) ? wf.props : undefined, entityNames);
390
+ if (props) {
391
+ if (props.name)
392
+ doc.name = props.name;
393
+ if (props.on)
394
+ doc.on = convertTriggerProps(props.on);
395
+ if (props.permissions)
396
+ doc.permissions = convertValueKeys(props.permissions);
397
+ if (props.env)
398
+ doc.env = props.env;
399
+ if (props.concurrency)
400
+ doc.concurrency = convertValueKeys(props.concurrency);
401
+ if (props.defaults)
402
+ doc.defaults = convertValueKeys(props.defaults);
403
+ }
404
+ // Attach all triggers to first workflow for now
405
+ if (i === 0 && triggers.length > 0) {
406
+ const onSection = doc.on ?? {};
407
+ for (const [, trigger] of triggers) {
408
+ const entityType = trigger.entityType;
409
+ const eventName = TRIGGER_TYPE_TO_EVENT[entityType];
410
+ if (!eventName)
411
+ continue;
412
+ const tProps = toYAMLValue(isResourceDeclarable(trigger) ? trigger.props : undefined, entityNames);
413
+ if (tProps && Object.keys(tProps).length > 0) {
414
+ onSection[eventName] = convertValueKeys(tProps);
415
+ }
416
+ else {
417
+ onSection[eventName] = null;
418
+ }
419
+ }
420
+ doc.on = onSection;
421
+ }
422
+ // Jobs: use Workflow.props.jobs when defined, otherwise fall back to standalone exports
423
+ // (standalone exports only assigned to first workflow, for backwards compat with composites)
424
+ const rawProps = (isResourceDeclarable(wf) ? wf.props : undefined);
425
+ const inlineJobs = buildInlineJobsSection(rawProps, entityNames);
426
+ const jobsSection = inlineJobs ?? (i === 0 ? buildStandaloneJobsSection(jobs, entityNames) : undefined);
427
+ if (jobsSection)
428
+ doc.jobs = jobsSection;
429
+ const content = emitYAMLDocument(doc, commentOf(wf));
430
+ if (i === 0) {
431
+ primary = content;
432
+ // primary is written to the -o path by the CLI; don't also add it to files[]
433
+ }
434
+ else {
435
+ files[`${toKebabCase(name)}.yml`] = content;
436
+ }
437
+ }
438
+ return { primary, files };
439
+ }
440
+ /**
441
+ * Convert trigger props to YAML-compatible form.
442
+ */
443
+ function convertTriggerProps(on) {
444
+ if (typeof on !== "object" || on === null)
445
+ return on;
446
+ if (Array.isArray(on))
447
+ return on;
448
+ const result = {};
449
+ for (const [key, value] of Object.entries(on)) {
450
+ const eventName = toSnakeCase(key);
451
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
452
+ result[eventName] = convertValueKeys(value);
453
+ }
454
+ else {
455
+ result[eventName] = value;
456
+ }
457
+ }
458
+ return result;
459
+ }
460
+ /**
461
+ * Emit a complete YAML document from a structured object.
462
+ */
463
+ function emitYAMLDocument(doc, header) {
464
+ const sections = [];
465
+ if (header)
466
+ sections.push(commentLines(header, 0));
467
+ // Emit in canonical order: name, run-name, on, permissions, env, concurrency, defaults, jobs
468
+ const order = ["name", "run-name", "on", "permissions", "env", "concurrency", "defaults", "jobs"];
469
+ const emitted = new Set();
470
+ for (const key of order) {
471
+ if (key in doc && doc[key] !== undefined) {
472
+ emitted.add(key);
473
+ const value = doc[key];
474
+ if (value === null) {
475
+ sections.push(`${key}:`);
476
+ }
477
+ else if (key === "jobs" && hasComments(value)) {
478
+ sections.push(emitJobs(value));
479
+ }
480
+ else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
481
+ sections.push(`${key}: ${yamlScalar(value)}`);
482
+ }
483
+ else {
484
+ sections.push(emitYAMLEntry(key, value));
485
+ }
486
+ }
487
+ }
488
+ // Remaining keys
489
+ for (const [key, value] of Object.entries(doc)) {
490
+ if (emitted.has(key) || value === undefined)
491
+ continue;
492
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
493
+ sections.push(`${key}: ${yamlScalar(value)}`);
494
+ }
495
+ else {
496
+ sections.push(emitYAMLEntry(key, value));
497
+ }
498
+ }
499
+ return unquotePinComments(sections.join("\n\n") + "\n");
500
+ }
501
+ /** Whether any job in a jobs section, or any of its steps, carries a comment. */
502
+ function hasComments(jobs) {
503
+ if (!jobs || typeof jobs !== "object")
504
+ return false;
505
+ return Object.values(jobs).some((job) => !!job && typeof job === "object" && (COMMENTS.has(job) || stepsOf(job).some((s) => !!s && typeof s === "object" && COMMENTS.has(s))));
506
+ }
507
+ const stepsOf = (job) => {
508
+ const steps = job.steps;
509
+ return Array.isArray(steps) ? steps : [];
510
+ };
511
+ /**
512
+ * The jobs section with each comment above its job or step. Everything else
513
+ * is what `emitYAMLEntry("jobs", jobs)` writes: the job at indent 1, its keys
514
+ * at 2, its step items at 3.
515
+ */
516
+ function emitJobs(jobs) {
517
+ const lines = ["jobs:"];
518
+ for (const [name, job] of Object.entries(jobs)) {
519
+ if (job === undefined)
520
+ continue;
521
+ if (!job || typeof job !== "object" || Array.isArray(job)) {
522
+ lines.push(emitYAMLEntry(name, job, 1));
523
+ continue;
524
+ }
525
+ const comment = COMMENTS.get(job);
526
+ if (comment)
527
+ lines.push(commentLines(comment, 1));
528
+ const steps = stepsOf(job);
529
+ if (!steps.some((s) => !!s && typeof s === "object" && COMMENTS.has(s))) {
530
+ lines.push(emitYAMLEntry(name, job, 1));
531
+ continue;
532
+ }
533
+ lines.push(` ${name}:`);
534
+ for (const [key, value] of Object.entries(job)) {
535
+ if (value === undefined)
536
+ continue;
537
+ if (key !== "steps") {
538
+ lines.push(emitYAMLEntry(key, value, 2));
539
+ continue;
540
+ }
541
+ lines.push(" steps:");
542
+ for (const step of steps) {
543
+ const stepComment = step && typeof step === "object" ? COMMENTS.get(step) : undefined;
544
+ if (stepComment)
545
+ lines.push(commentLines(stepComment, 3));
546
+ lines.push(emitYAML([step], 3).slice(1));
547
+ }
548
+ }
549
+ }
550
+ return lines.join("\n");
551
+ }
552
+ /**
553
+ * A SHA-pinned `uses:` carries its version as a trailing comment
554
+ * (`actions/checkout@<sha> # v7.0.1`). The YAML emitter quotes any string with
555
+ * a `#`, which would make the comment part of the ref GitHub resolves. A ref
556
+ * holds no whitespace, so `<ref> # <label>` inside quotes is always this
557
+ * pattern, and writing it unquoted turns the label back into a comment.
558
+ */
559
+ function unquotePinComments(yaml) {
560
+ return yaml.replace(/^(\s*(?:- )?uses: )'([^\s']+) # ([^'\n]*)'$/gm, "$1$2 # $3");
561
+ }
562
+ function yamlScalar(value) {
563
+ if (typeof value === "boolean")
564
+ return value ? "true" : "false";
565
+ if (typeof value === "number")
566
+ return String(value);
567
+ // Quote strings that might be ambiguous
568
+ if (/^[\d]|[:#\[\]{}|>&*!%@`]|true|false|null|yes|no/i.test(value)) {
569
+ return `'${value.replace(/'/g, "''")}'`;
570
+ }
571
+ return value;
572
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * GitHub Actions workflow schema fetching — downloads the JSON Schema
3
+ * and caches it locally.
4
+ */
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { fetchWithCache, clearCacheFile } from "@intentius/chant/codegen/fetch";
8
+ /**
9
+ * Schema URL from SchemaStore.
10
+ */
11
+ const SCHEMA_URL = "https://json.schemastore.org/github-workflow.json";
12
+ /**
13
+ * Get the cache file path for the workflow schema.
14
+ */
15
+ export function getCachePath() {
16
+ return join(homedir(), ".chant", "github-workflow-schema.json");
17
+ }
18
+ /**
19
+ * Fetch the GitHub Actions Workflow JSON Schema.
20
+ * Uses local file caching with 24-hour TTL.
21
+ */
22
+ export async function fetchWorkflowSchema(force) {
23
+ return fetchWithCache({
24
+ url: SCHEMA_URL,
25
+ cacheFile: getCachePath(),
26
+ }, force);
27
+ }
28
+ /**
29
+ * Fetch the workflow schema as a Map<typeName, Buffer>
30
+ * compatible with the generatePipeline fetchSchemas callback.
31
+ *
32
+ * Single document keyed by "GitHub::Actions::Workflow" — the parse
33
+ * step will split it into multiple entities.
34
+ */
35
+ export async function fetchSchemas(force) {
36
+ const data = await fetchWorkflowSchema(force);
37
+ const schemas = new Map();
38
+ schemas.set("GitHub::Actions::Workflow", data);
39
+ return schemas;
40
+ }
41
+ /**
42
+ * Clear the cached schema file.
43
+ */
44
+ export function clearCache() {
45
+ clearCacheFile(getCachePath());
46
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Thin entry point for `npm run validate` in lexicon-github.
4
+ */
5
+ import { validate } from "./validate.js";
6
+ const result = await validate();
7
+ for (const check of result.checks) {
8
+ const status = check.ok ? "OK" : "FAIL";
9
+ const msg = check.error ? ` — ${check.error}` : "";
10
+ console.error(` [${status}] ${check.name}${msg}`);
11
+ }
12
+ if (!result.success) {
13
+ console.error("Validation failed");
14
+ process.exit(1);
15
+ }
16
+ console.error("All validation checks passed.");
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Validate generated lexicon-github artifacts.
3
+ */
4
+ import { dirname, join } from "path";
5
+ import { existsSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import { validateLexiconArtifacts } from "@intentius/chant/codegen/validate";
8
+ const REQUIRED_NAMES = [
9
+ "Workflow", "Job", "ReusableWorkflowCallJob",
10
+ "Step", "Strategy", "Permissions", "Concurrency",
11
+ "Container", "Service", "Environment", "Defaults",
12
+ "PushTrigger", "PullRequestTrigger", "PullRequestTargetTrigger",
13
+ "ScheduleTrigger", "WorkflowDispatchTrigger", "WorkflowCallTrigger",
14
+ "WorkflowRunTrigger", "RepositoryDispatchTrigger",
15
+ "WorkflowInput", "WorkflowOutput", "WorkflowSecret",
16
+ ];
17
+ /**
18
+ * Validate the generated lexicon-github artifacts.
19
+ */
20
+ export async function validate(opts) {
21
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
22
+ const generatedDir = join(basePath, "src", "generated");
23
+ if (!existsSync(generatedDir)) {
24
+ return {
25
+ success: false,
26
+ checks: [{
27
+ name: "generated-dir",
28
+ ok: false,
29
+ error: 'src/generated/ not found — run "chant dev generate" first',
30
+ }],
31
+ };
32
+ }
33
+ return validateLexiconArtifacts({
34
+ lexiconJsonFilename: "lexicon-github.json",
35
+ requiredNames: REQUIRED_NAMES,
36
+ basePath,
37
+ });
38
+ }