@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.
Files changed (106) hide show
  1. package/dist/integrity.json +31 -0
  2. package/dist/manifest.json +15 -0
  3. package/dist/meta.json +135 -0
  4. package/dist/rules/deprecated-action-version.ts +49 -0
  5. package/dist/rules/detect-secrets.ts +53 -0
  6. package/dist/rules/extract-inline-structs.ts +62 -0
  7. package/dist/rules/file-job-limit.ts +49 -0
  8. package/dist/rules/gha006.ts +58 -0
  9. package/dist/rules/gha009.ts +42 -0
  10. package/dist/rules/gha011.ts +40 -0
  11. package/dist/rules/gha017.ts +32 -0
  12. package/dist/rules/gha018.ts +40 -0
  13. package/dist/rules/gha019.ts +72 -0
  14. package/dist/rules/job-timeout.ts +59 -0
  15. package/dist/rules/missing-recommended-inputs.ts +61 -0
  16. package/dist/rules/no-hardcoded-secrets.ts +46 -0
  17. package/dist/rules/no-raw-expressions.ts +51 -0
  18. package/dist/rules/suggest-cache.ts +71 -0
  19. package/dist/rules/use-condition-builders.ts +45 -0
  20. package/dist/rules/use-matrix-builder.ts +44 -0
  21. package/dist/rules/use-typed-actions.ts +47 -0
  22. package/dist/rules/validate-concurrency.ts +66 -0
  23. package/dist/rules/yaml-helpers.ts +129 -0
  24. package/dist/skills/chant-github.md +29 -0
  25. package/dist/skills/github-actions-patterns.md +93 -0
  26. package/dist/types/index.d.ts +358 -0
  27. package/package.json +33 -0
  28. package/src/codegen/docs-cli.ts +3 -0
  29. package/src/codegen/docs.ts +1138 -0
  30. package/src/codegen/generate-cli.ts +36 -0
  31. package/src/codegen/generate-lexicon.ts +58 -0
  32. package/src/codegen/generate-typescript.ts +149 -0
  33. package/src/codegen/generate.ts +141 -0
  34. package/src/codegen/naming.ts +57 -0
  35. package/src/codegen/package.ts +65 -0
  36. package/src/codegen/parse.ts +700 -0
  37. package/src/codegen/patches.ts +46 -0
  38. package/src/composites/cache.ts +25 -0
  39. package/src/composites/checkout.ts +31 -0
  40. package/src/composites/composites.test.ts +675 -0
  41. package/src/composites/deploy-environment.ts +77 -0
  42. package/src/composites/docker-build.ts +120 -0
  43. package/src/composites/download-artifact.ts +24 -0
  44. package/src/composites/go-ci.ts +91 -0
  45. package/src/composites/index.ts +26 -0
  46. package/src/composites/node-ci.ts +71 -0
  47. package/src/composites/node-pipeline.ts +151 -0
  48. package/src/composites/python-ci.ts +92 -0
  49. package/src/composites/setup-go.ts +24 -0
  50. package/src/composites/setup-node.ts +26 -0
  51. package/src/composites/setup-python.ts +24 -0
  52. package/src/composites/upload-artifact.ts +27 -0
  53. package/src/coverage.ts +49 -0
  54. package/src/expression.test.ts +147 -0
  55. package/src/expression.ts +214 -0
  56. package/src/generated/index.d.ts +358 -0
  57. package/src/generated/index.ts +29 -0
  58. package/src/generated/lexicon-github.json +135 -0
  59. package/src/generated/runtime.ts +4 -0
  60. package/src/import/generator.test.ts +110 -0
  61. package/src/import/generator.ts +119 -0
  62. package/src/import/parser.test.ts +98 -0
  63. package/src/import/parser.ts +73 -0
  64. package/src/index.ts +53 -0
  65. package/src/lint/post-synth/gha006.ts +58 -0
  66. package/src/lint/post-synth/gha009.ts +42 -0
  67. package/src/lint/post-synth/gha011.ts +40 -0
  68. package/src/lint/post-synth/gha017.ts +32 -0
  69. package/src/lint/post-synth/gha018.ts +40 -0
  70. package/src/lint/post-synth/gha019.ts +72 -0
  71. package/src/lint/post-synth/post-synth.test.ts +318 -0
  72. package/src/lint/post-synth/yaml-helpers.ts +129 -0
  73. package/src/lint/rules/data/deprecated-versions.ts +13 -0
  74. package/src/lint/rules/data/known-actions.ts +13 -0
  75. package/src/lint/rules/data/recommended-inputs.ts +10 -0
  76. package/src/lint/rules/data/secret-patterns.ts +31 -0
  77. package/src/lint/rules/deprecated-action-version.ts +49 -0
  78. package/src/lint/rules/detect-secrets.ts +53 -0
  79. package/src/lint/rules/extract-inline-structs.ts +62 -0
  80. package/src/lint/rules/file-job-limit.ts +49 -0
  81. package/src/lint/rules/index.ts +17 -0
  82. package/src/lint/rules/job-timeout.ts +59 -0
  83. package/src/lint/rules/missing-recommended-inputs.ts +61 -0
  84. package/src/lint/rules/no-hardcoded-secrets.ts +46 -0
  85. package/src/lint/rules/no-raw-expressions.ts +51 -0
  86. package/src/lint/rules/rules.test.ts +365 -0
  87. package/src/lint/rules/suggest-cache.ts +71 -0
  88. package/src/lint/rules/use-condition-builders.ts +45 -0
  89. package/src/lint/rules/use-matrix-builder.ts +44 -0
  90. package/src/lint/rules/use-typed-actions.ts +47 -0
  91. package/src/lint/rules/validate-concurrency.ts +66 -0
  92. package/src/lsp/completions.test.ts +9 -0
  93. package/src/lsp/completions.ts +20 -0
  94. package/src/lsp/hover.test.ts +9 -0
  95. package/src/lsp/hover.ts +38 -0
  96. package/src/package-cli.ts +42 -0
  97. package/src/plugin.test.ts +128 -0
  98. package/src/plugin.ts +408 -0
  99. package/src/serializer.test.ts +270 -0
  100. package/src/serializer.ts +383 -0
  101. package/src/skills/github-actions-patterns.md +93 -0
  102. package/src/spec/fetch.ts +55 -0
  103. package/src/validate-cli.ts +19 -0
  104. package/src/validate.test.ts +12 -0
  105. package/src/validate.ts +32 -0
  106. package/src/variables.ts +44 -0
@@ -0,0 +1,383 @@
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
+
9
+ import type { Declarable } from "@intentius/chant/declarable";
10
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
11
+ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
12
+ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
13
+ import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
14
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
15
+ import { emitYAML } from "@intentius/chant/yaml";
16
+
17
+ // ── Key conversion ────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Convert camelCase property names to kebab-case for YAML output.
21
+ * Examples: timeoutMinutes → timeout-minutes, runsOn → runs-on
22
+ */
23
+ function toKebabCase(name: string): string {
24
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
25
+ }
26
+
27
+ /**
28
+ * Convert camelCase trigger names to snake_case event names.
29
+ * Examples: pullRequest → pull_request, workflowDispatch → workflow_dispatch
30
+ */
31
+ function toSnakeCase(name: string): string {
32
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
33
+ }
34
+
35
+ /**
36
+ * Map entity type names to YAML trigger event names.
37
+ */
38
+ const TRIGGER_TYPE_TO_EVENT: Record<string, string> = {
39
+ "GitHub::Actions::PushTrigger": "push",
40
+ "GitHub::Actions::PullRequestTrigger": "pull_request",
41
+ "GitHub::Actions::PullRequestTargetTrigger": "pull_request_target",
42
+ "GitHub::Actions::ScheduleTrigger": "schedule",
43
+ "GitHub::Actions::WorkflowDispatchTrigger": "workflow_dispatch",
44
+ "GitHub::Actions::WorkflowCallTrigger": "workflow_call",
45
+ "GitHub::Actions::WorkflowRunTrigger": "workflow_run",
46
+ "GitHub::Actions::RepositoryDispatchTrigger": "repository_dispatch",
47
+ };
48
+
49
+ /** Check if an entity type is a trigger. */
50
+ function isTriggerType(entityType: string): boolean {
51
+ return entityType in TRIGGER_TYPE_TO_EVENT;
52
+ }
53
+
54
+ // ── Visitor ───────────────────────────────────────────────────────
55
+
56
+ function githubVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
57
+ return {
58
+ attrRef: (name, _attr) => name,
59
+ resourceRef: (name) => toKebabCase(name),
60
+ propertyDeclarable: (entity, walk) => {
61
+ if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
62
+ return undefined;
63
+ }
64
+ const props = entity.props as Record<string, unknown>;
65
+ const result: Record<string, unknown> = {};
66
+ for (const [key, value] of Object.entries(props)) {
67
+ if (value !== undefined) {
68
+ result[key] = walk(value);
69
+ }
70
+ }
71
+ return Object.keys(result).length > 0 ? result : undefined;
72
+ },
73
+ };
74
+ }
75
+
76
+ // ── Intrinsic preprocessing ───────────────────────────────────────
77
+
78
+ function preprocessIntrinsics(value: unknown): unknown {
79
+ if (value === null || value === undefined) return value;
80
+
81
+ if (typeof value === "object" && INTRINSIC_MARKER in value) {
82
+ if ("toYAML" in value && typeof value.toYAML === "function") {
83
+ return (value as { toYAML(): unknown }).toYAML();
84
+ }
85
+ }
86
+
87
+ // Leave Declarables untouched
88
+ if (typeof value === "object" && value !== null && "entityType" in value) {
89
+ return value;
90
+ }
91
+
92
+ if (Array.isArray(value)) {
93
+ return value.map(preprocessIntrinsics);
94
+ }
95
+
96
+ if (typeof value === "object") {
97
+ const result: Record<string, unknown> = {};
98
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
99
+ result[k] = preprocessIntrinsics(v);
100
+ }
101
+ return result;
102
+ }
103
+
104
+ return value;
105
+ }
106
+
107
+ function toYAMLValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
108
+ const preprocessed = preprocessIntrinsics(value);
109
+ return walkValue(preprocessed, entityNames, githubVisitor(entityNames));
110
+ }
111
+
112
+ // ── Key conversion for YAML output ────────────────────────────────
113
+
114
+ /**
115
+ * Convert a props object keys from camelCase to kebab-case for job/step properties.
116
+ */
117
+ function convertKeys(obj: Record<string, unknown>): Record<string, unknown> {
118
+ const result: Record<string, unknown> = {};
119
+ for (const [key, value] of Object.entries(obj)) {
120
+ if (value === undefined || value === null) continue;
121
+ // Zero and false are valid values, but undefined/null should be omitted
122
+ const yamlKey = toKebabCase(key);
123
+ result[yamlKey] = convertValueKeys(value);
124
+ }
125
+ return result;
126
+ }
127
+
128
+ function convertValueKeys(value: unknown): unknown {
129
+ if (value === null || value === undefined) return value;
130
+ if (typeof value !== "object") return value;
131
+ if (Array.isArray(value)) return value.map(convertValueKeys);
132
+
133
+ const obj = value as Record<string, unknown>;
134
+ const result: Record<string, unknown> = {};
135
+ for (const [key, v] of Object.entries(obj)) {
136
+ if (v === undefined || v === null) continue;
137
+ result[toKebabCase(key)] = convertValueKeys(v);
138
+ }
139
+ return result;
140
+ }
141
+
142
+ // ── Serializer ────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * GitHub Actions YAML serializer implementation.
146
+ */
147
+ export const githubSerializer: Serializer = {
148
+ name: "github",
149
+ rulePrefix: "GHA",
150
+
151
+ serialize(
152
+ entities: Map<string, Declarable>,
153
+ _outputs?: LexiconOutput[],
154
+ ): string | SerializerResult {
155
+ const entityNames = new Map<Declarable, string>();
156
+ for (const [name, entity] of entities) {
157
+ entityNames.set(entity, name);
158
+ }
159
+
160
+ // Categorize entities
161
+ const workflows: Array<[string, Declarable]> = [];
162
+ const jobs: Array<[string, Declarable]> = [];
163
+ const triggers: Array<[string, Declarable]> = [];
164
+ const others: Array<[string, Declarable]> = [];
165
+
166
+ for (const [name, entity] of entities) {
167
+ if (isPropertyDeclarable(entity)) continue;
168
+
169
+ const entityType = (entity as Record<string, unknown>).entityType as string;
170
+ if (entityType === "GitHub::Actions::Workflow") {
171
+ workflows.push([name, entity]);
172
+ } else if (entityType === "GitHub::Actions::Job" || entityType === "GitHub::Actions::ReusableWorkflowCallJob") {
173
+ jobs.push([name, entity]);
174
+ } else if (isTriggerType(entityType)) {
175
+ triggers.push([name, entity]);
176
+ } else {
177
+ others.push([name, entity]);
178
+ }
179
+ }
180
+
181
+ // If multiple workflows, produce multiple files
182
+ if (workflows.length > 1) {
183
+ return serializeMultiWorkflow(workflows, jobs, triggers, entities, entityNames);
184
+ }
185
+
186
+ // Single workflow (or implicit workflow from jobs)
187
+ return serializeSingleWorkflow(workflows, jobs, triggers, entities, entityNames);
188
+ },
189
+ };
190
+
191
+ function serializeSingleWorkflow(
192
+ workflows: Array<[string, Declarable]>,
193
+ jobs: Array<[string, Declarable]>,
194
+ triggers: Array<[string, Declarable]>,
195
+ _entities: Map<string, Declarable>,
196
+ entityNames: Map<Declarable, string>,
197
+ ): string {
198
+ const doc: Record<string, unknown> = {};
199
+
200
+ // Workflow-level properties
201
+ if (workflows.length > 0) {
202
+ const [, wf] = workflows[0];
203
+ const props = toYAMLValue((wf as Record<string, unknown>).props, entityNames) as Record<string, unknown> | undefined;
204
+ if (props) {
205
+ if (props.name) doc.name = props.name;
206
+ if (props["run-name"] || props.runName) doc["run-name"] = props["run-name"] ?? props.runName;
207
+ if (props.permissions) doc.permissions = convertValueKeys(props.permissions);
208
+ if (props.env) doc.env = props.env;
209
+ if (props.concurrency) doc.concurrency = convertValueKeys(props.concurrency);
210
+ if (props.defaults) doc.defaults = convertValueKeys(props.defaults);
211
+
212
+ // Handle 'on' from workflow props
213
+ if (props.on) {
214
+ doc.on = convertTriggerProps(props.on);
215
+ }
216
+ }
217
+ }
218
+
219
+ // If triggers exist as separate entities, merge into 'on'
220
+ if (triggers.length > 0) {
221
+ const onSection = (doc.on as Record<string, unknown>) ?? {};
222
+ for (const [, trigger] of triggers) {
223
+ const entityType = (trigger as Record<string, unknown>).entityType as string;
224
+ const eventName = TRIGGER_TYPE_TO_EVENT[entityType];
225
+ if (!eventName) continue;
226
+
227
+ const props = toYAMLValue((trigger as Record<string, unknown>).props, entityNames) as Record<string, unknown> | undefined;
228
+ if (props && Object.keys(props).length > 0) {
229
+ onSection[eventName] = convertValueKeys(props);
230
+ } else {
231
+ onSection[eventName] = null;
232
+ }
233
+ }
234
+ doc.on = onSection;
235
+ }
236
+
237
+ // Jobs
238
+ if (jobs.length > 0) {
239
+ const jobsSection: Record<string, unknown> = {};
240
+ for (const [name, job] of jobs) {
241
+ const props = toYAMLValue((job as Record<string, unknown>).props, entityNames) as Record<string, unknown> | undefined;
242
+ if (props) {
243
+ const yamlName = toKebabCase(name);
244
+ jobsSection[yamlName] = convertKeys(props);
245
+ }
246
+ }
247
+ doc.jobs = jobsSection;
248
+ }
249
+
250
+ return emitYAMLDocument(doc);
251
+ }
252
+
253
+ function serializeMultiWorkflow(
254
+ workflows: Array<[string, Declarable]>,
255
+ jobs: Array<[string, Declarable]>,
256
+ triggers: Array<[string, Declarable]>,
257
+ _entities: Map<string, Declarable>,
258
+ entityNames: Map<Declarable, string>,
259
+ ): SerializerResult {
260
+ // For multi-workflow, each workflow gets its own file.
261
+ // Jobs and triggers need to be associated with workflows somehow.
262
+ // For now, first workflow gets all unscoped entities.
263
+ const files: Record<string, string> = {};
264
+ let primary = "";
265
+
266
+ for (let i = 0; i < workflows.length; i++) {
267
+ const [name, wf] = workflows[i];
268
+ const doc: Record<string, unknown> = {};
269
+ const props = toYAMLValue((wf as Record<string, unknown>).props, entityNames) as Record<string, unknown> | undefined;
270
+
271
+ if (props) {
272
+ if (props.name) doc.name = props.name;
273
+ if (props.on) doc.on = convertTriggerProps(props.on);
274
+ if (props.permissions) doc.permissions = convertValueKeys(props.permissions);
275
+ if (props.env) doc.env = props.env;
276
+ if (props.concurrency) doc.concurrency = convertValueKeys(props.concurrency);
277
+ if (props.defaults) doc.defaults = convertValueKeys(props.defaults);
278
+ }
279
+
280
+ // Attach all triggers to first workflow for now
281
+ if (i === 0 && triggers.length > 0) {
282
+ const onSection = (doc.on as Record<string, unknown>) ?? {};
283
+ for (const [, trigger] of triggers) {
284
+ const entityType = (trigger as Record<string, unknown>).entityType as string;
285
+ const eventName = TRIGGER_TYPE_TO_EVENT[entityType];
286
+ if (!eventName) continue;
287
+ const tProps = toYAMLValue((trigger as Record<string, unknown>).props, entityNames) as Record<string, unknown> | undefined;
288
+ if (tProps && Object.keys(tProps).length > 0) {
289
+ onSection[eventName] = convertValueKeys(tProps);
290
+ } else {
291
+ onSection[eventName] = null;
292
+ }
293
+ }
294
+ doc.on = onSection;
295
+ }
296
+
297
+ // Attach all jobs to first workflow for now
298
+ if (i === 0 && jobs.length > 0) {
299
+ const jobsSection: Record<string, unknown> = {};
300
+ for (const [jName, job] of jobs) {
301
+ const jProps = toYAMLValue((job as Record<string, unknown>).props, entityNames) as Record<string, unknown> | undefined;
302
+ if (jProps) {
303
+ jobsSection[toKebabCase(jName)] = convertKeys(jProps);
304
+ }
305
+ }
306
+ doc.jobs = jobsSection;
307
+ }
308
+
309
+ const content = emitYAMLDocument(doc);
310
+ const fileName = `${toKebabCase(name)}.yml`;
311
+ files[fileName] = content;
312
+
313
+ if (i === 0) primary = content;
314
+ }
315
+
316
+ return { primary, files };
317
+ }
318
+
319
+ /**
320
+ * Convert trigger props to YAML-compatible form.
321
+ */
322
+ function convertTriggerProps(on: unknown): unknown {
323
+ if (typeof on !== "object" || on === null) return on;
324
+ if (Array.isArray(on)) return on;
325
+
326
+ const result: Record<string, unknown> = {};
327
+ for (const [key, value] of Object.entries(on as Record<string, unknown>)) {
328
+ const eventName = toSnakeCase(key);
329
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
330
+ result[eventName] = convertValueKeys(value as Record<string, unknown>);
331
+ } else {
332
+ result[eventName] = value;
333
+ }
334
+ }
335
+ return result;
336
+ }
337
+
338
+ /**
339
+ * Emit a complete YAML document from a structured object.
340
+ */
341
+ function emitYAMLDocument(doc: Record<string, unknown>): string {
342
+ const sections: string[] = [];
343
+
344
+ // Emit in canonical order: name, run-name, on, permissions, env, concurrency, defaults, jobs
345
+ const order = ["name", "run-name", "on", "permissions", "env", "concurrency", "defaults", "jobs"];
346
+ const emitted = new Set<string>();
347
+
348
+ for (const key of order) {
349
+ if (key in doc && doc[key] !== undefined) {
350
+ emitted.add(key);
351
+ const value = doc[key];
352
+ if (value === null) {
353
+ sections.push(`${key}:`);
354
+ } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
355
+ sections.push(`${key}: ${yamlScalar(value)}`);
356
+ } else {
357
+ sections.push(`${key}:` + emitYAML(value, 1));
358
+ }
359
+ }
360
+ }
361
+
362
+ // Remaining keys
363
+ for (const [key, value] of Object.entries(doc)) {
364
+ if (emitted.has(key) || value === undefined) continue;
365
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
366
+ sections.push(`${key}: ${yamlScalar(value)}`);
367
+ } else {
368
+ sections.push(`${key}:` + emitYAML(value, 1));
369
+ }
370
+ }
371
+
372
+ return sections.join("\n\n") + "\n";
373
+ }
374
+
375
+ function yamlScalar(value: string | number | boolean): string {
376
+ if (typeof value === "boolean") return value ? "true" : "false";
377
+ if (typeof value === "number") return String(value);
378
+ // Quote strings that might be ambiguous
379
+ if (/^[\d]|[:#\[\]{}|>&*!%@`]|true|false|null|yes|no/i.test(value)) {
380
+ return `'${value.replace(/'/g, "''")}'`;
381
+ }
382
+ return value;
383
+ }
@@ -0,0 +1,93 @@
1
+ ---
2
+ skill: github-actions-patterns
3
+ description: GitHub Actions workflow patterns — triggers, jobs, matrix, caching, artifacts, permissions, reusable workflows
4
+ ---
5
+
6
+ # GitHub Actions Patterns
7
+
8
+ ## Workflow Structure
9
+
10
+ A GitHub Actions workflow is a YAML file in `.github/workflows/`. Key sections:
11
+
12
+ - `name:` — display name for the workflow
13
+ - `on:` — trigger events (push, pull_request, schedule, etc.)
14
+ - `permissions:` — GITHUB_TOKEN permissions (least-privilege)
15
+ - `jobs:` — named jobs that run on runners
16
+
17
+ ## Trigger Patterns
18
+
19
+ ```typescript
20
+ // Push to main
21
+ new PushTrigger({ branches: ["main"] })
22
+
23
+ // Pull requests
24
+ new PullRequestTrigger({ branches: ["main"], types: ["opened", "synchronize"] })
25
+
26
+ // Schedule (cron)
27
+ new ScheduleTrigger({ cron: "0 0 * * *" })
28
+
29
+ // Manual dispatch with inputs
30
+ new WorkflowDispatchTrigger({ inputs: { environment: { type: "choice", options: ["staging", "production"] } } })
31
+ ```
32
+
33
+ ## Matrix Strategy
34
+
35
+ ```typescript
36
+ new Strategy({
37
+ matrix: {
38
+ os: ["ubuntu-latest", "windows-latest"],
39
+ "node-version": ["18", "20", "22"],
40
+ },
41
+ "fail-fast": false,
42
+ })
43
+ ```
44
+
45
+ ## Caching
46
+
47
+ Use the `cache` option on setup actions, or explicit Cache action:
48
+ ```typescript
49
+ SetupNode({ nodeVersion: "22", cache: "npm" })
50
+ ```
51
+
52
+ ## Permissions (Least Privilege)
53
+
54
+ Always set explicit permissions:
55
+ ```typescript
56
+ new Permissions({
57
+ contents: "read",
58
+ "pull-requests": "write",
59
+ })
60
+ ```
61
+
62
+ ## Reusable Workflows
63
+
64
+ Call reusable workflows with `ReusableWorkflowCallJob`:
65
+ ```typescript
66
+ new ReusableWorkflowCallJob({
67
+ uses: "./.github/workflows/deploy.yml",
68
+ with: { environment: "production" },
69
+ secrets: "inherit",
70
+ })
71
+ ```
72
+
73
+ ## Artifacts
74
+
75
+ ```typescript
76
+ UploadArtifact({ name: "build", path: "dist/", retentionDays: 7 })
77
+ DownloadArtifact({ name: "build", path: "dist/" })
78
+ ```
79
+
80
+ ## Environment Protection
81
+
82
+ ```typescript
83
+ new Environment({ name: "production", url: "https://example.com" })
84
+ ```
85
+
86
+ ## Concurrency
87
+
88
+ ```typescript
89
+ new Concurrency({
90
+ group: "${{ github.workflow }}-${{ github.ref }}",
91
+ "cancel-in-progress": true,
92
+ })
93
+ ```
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GitHub Actions workflow schema fetching — downloads the JSON Schema
3
+ * and caches it locally.
4
+ */
5
+
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import { fetchWithCache, clearCacheFile } from "@intentius/chant/codegen/fetch";
9
+
10
+ /**
11
+ * Schema URL from SchemaStore.
12
+ */
13
+ const SCHEMA_URL = "https://json.schemastore.org/github-workflow.json";
14
+
15
+ /**
16
+ * Get the cache file path for the workflow schema.
17
+ */
18
+ export function getCachePath(): string {
19
+ return join(homedir(), ".chant", "github-workflow-schema.json");
20
+ }
21
+
22
+ /**
23
+ * Fetch the GitHub Actions Workflow JSON Schema.
24
+ * Uses local file caching with 24-hour TTL.
25
+ */
26
+ export async function fetchWorkflowSchema(force?: boolean): Promise<Buffer> {
27
+ return fetchWithCache(
28
+ {
29
+ url: SCHEMA_URL,
30
+ cacheFile: getCachePath(),
31
+ },
32
+ force,
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Fetch the workflow schema as a Map<typeName, Buffer>
38
+ * compatible with the generatePipeline fetchSchemas callback.
39
+ *
40
+ * Single document keyed by "GitHub::Actions::Workflow" — the parse
41
+ * step will split it into multiple entities.
42
+ */
43
+ export async function fetchSchemas(force?: boolean): Promise<Map<string, Buffer>> {
44
+ const data = await fetchWorkflowSchema(force);
45
+ const schemas = new Map<string, Buffer>();
46
+ schemas.set("GitHub::Actions::Workflow", data);
47
+ return schemas;
48
+ }
49
+
50
+ /**
51
+ * Clear the cached schema file.
52
+ */
53
+ export function clearCache(): void {
54
+ clearCacheFile(getCachePath());
55
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Thin entry point for `bun run validate` in lexicon-github.
4
+ */
5
+ import { validate } from "./validate";
6
+
7
+ const result = await validate();
8
+
9
+ for (const check of result.checks) {
10
+ const status = check.ok ? "OK" : "FAIL";
11
+ const msg = check.error ? ` — ${check.error}` : "";
12
+ console.error(` [${status}] ${check.name}${msg}`);
13
+ }
14
+
15
+ if (!result.success) {
16
+ console.error("Validation failed");
17
+ process.exit(1);
18
+ }
19
+ console.error("All validation checks passed.");
@@ -0,0 +1,12 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { validate } from "./validate";
3
+
4
+ describe("validate", () => {
5
+ test("exports validate function", () => {
6
+ expect(typeof validate).toBe("function");
7
+ });
8
+
9
+ // Note: Full validation requires generated artifacts to exist.
10
+ // This test verifies the validate module loads correctly.
11
+ // Run `bun run generate` first for full validation tests.
12
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Validate generated lexicon-github artifacts.
3
+ */
4
+
5
+ import { dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { validateLexiconArtifacts, type ValidateResult } from "@intentius/chant/codegen/validate";
8
+
9
+ export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
10
+
11
+ const REQUIRED_NAMES = [
12
+ "Workflow", "Job", "ReusableWorkflowCallJob",
13
+ "Step", "Strategy", "Permissions", "Concurrency",
14
+ "Container", "Service", "Environment", "Defaults",
15
+ "PushTrigger", "PullRequestTrigger", "PullRequestTargetTrigger",
16
+ "ScheduleTrigger", "WorkflowDispatchTrigger", "WorkflowCallTrigger",
17
+ "WorkflowRunTrigger", "RepositoryDispatchTrigger",
18
+ "WorkflowInput", "WorkflowOutput", "WorkflowSecret",
19
+ ];
20
+
21
+ /**
22
+ * Validate the generated lexicon-github artifacts.
23
+ */
24
+ export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
25
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
26
+
27
+ return validateLexiconArtifacts({
28
+ lexiconJsonFilename: "lexicon-github.json",
29
+ requiredNames: REQUIRED_NAMES,
30
+ basePath,
31
+ });
32
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GitHub Actions predefined context variable references.
3
+ *
4
+ * These provide type-safe access to GitHub and Runner context values
5
+ * that expand to `${{ context.property }}` expressions in YAML.
6
+ */
7
+
8
+ import { Expression } from "./expression";
9
+
10
+ export const GitHub = {
11
+ Ref: new Expression("github.ref"),
12
+ RefName: new Expression("github.ref_name"),
13
+ RefType: new Expression("github.ref_type"),
14
+ Sha: new Expression("github.sha"),
15
+ Actor: new Expression("github.actor"),
16
+ TriggeringActor: new Expression("github.triggering_actor"),
17
+ Repository: new Expression("github.repository"),
18
+ RepositoryOwner: new Expression("github.repository_owner"),
19
+ EventName: new Expression("github.event_name"),
20
+ Event: new Expression("github.event"),
21
+ RunId: new Expression("github.run_id"),
22
+ RunNumber: new Expression("github.run_number"),
23
+ RunAttempt: new Expression("github.run_attempt"),
24
+ Workflow: new Expression("github.workflow"),
25
+ WorkflowRef: new Expression("github.workflow_ref"),
26
+ Workspace: new Expression("github.workspace"),
27
+ Token: new Expression("github.token"),
28
+ Job: new Expression("github.job"),
29
+ HeadRef: new Expression("github.head_ref"),
30
+ BaseRef: new Expression("github.base_ref"),
31
+ ServerUrl: new Expression("github.server_url"),
32
+ ApiUrl: new Expression("github.api_url"),
33
+ GraphqlUrl: new Expression("github.graphql_url"),
34
+ Action: new Expression("github.action"),
35
+ ActionPath: new Expression("github.action_path"),
36
+ } as const;
37
+
38
+ export const Runner = {
39
+ Os: new Expression("runner.os"),
40
+ Arch: new Expression("runner.arch"),
41
+ Name: new Expression("runner.name"),
42
+ Temp: new Expression("runner.temp"),
43
+ ToolCache: new Expression("runner.tool_cache"),
44
+ } as const;