@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,71 @@
1
+ /**
2
+ * GHA015: Suggest Cache
3
+ *
4
+ * Flags setup action composites in steps without a corresponding Cache composite.
5
+ */
6
+
7
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
8
+ import * as ts from "typescript";
9
+
10
+ const setupActionsNeedingCache = new Set(["SetupNode", "SetupGo", "SetupPython"]);
11
+
12
+ export const suggestCacheRule: LintRule = {
13
+ id: "GHA015",
14
+ severity: "warning",
15
+ category: "performance",
16
+ description: "Setup action should be paired with a Cache composite for faster builds",
17
+
18
+ check(context: LintContext): LintDiagnostic[] {
19
+ const { sourceFile } = context;
20
+ const diagnostics: LintDiagnostic[] = [];
21
+
22
+ function visit(node: ts.Node): void {
23
+ // Look for steps array with setup actions but no Cache
24
+ if (
25
+ ts.isPropertyAssignment(node) &&
26
+ ts.isIdentifier(node.name) &&
27
+ node.name.text === "steps" &&
28
+ ts.isArrayLiteralExpression(node.initializer)
29
+ ) {
30
+ const elements = node.initializer.elements;
31
+ let hasSetup = false;
32
+ let setupName = "";
33
+ let hasCache = false;
34
+
35
+ for (const el of elements) {
36
+ if (ts.isCallExpression(el)) {
37
+ const name = ts.isIdentifier(el.expression) ? el.expression.text : "";
38
+ if (setupActionsNeedingCache.has(name)) {
39
+ hasSetup = true;
40
+ setupName = name;
41
+ // Check if the setup action already has cache in its props
42
+ if (el.arguments.length > 0 && ts.isObjectLiteralExpression(el.arguments[0])) {
43
+ const hasCacheProp = el.arguments[0].properties.some(
44
+ (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "cache",
45
+ );
46
+ if (hasCacheProp) hasCache = true;
47
+ }
48
+ }
49
+ if (name === "Cache") hasCache = true;
50
+ }
51
+ }
52
+
53
+ if (hasSetup && !hasCache) {
54
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
55
+ diagnostics.push({
56
+ file: sourceFile.fileName,
57
+ line: line + 1,
58
+ column: character + 1,
59
+ ruleId: "GHA015",
60
+ severity: "warning",
61
+ message: `${setupName}() found without Cache. Add a Cache() step or use the built-in cache option for faster builds.`,
62
+ });
63
+ }
64
+ }
65
+ ts.forEachChild(node, visit);
66
+ }
67
+
68
+ visit(sourceFile);
69
+ return diagnostics;
70
+ },
71
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * GHA002: Use Condition Builders
3
+ *
4
+ * Flags string literals containing `${{` in `if` property assignments
5
+ * inside Job/Step constructors. Suggests using Expression helpers.
6
+ */
7
+
8
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
+ import * as ts from "typescript";
10
+
11
+ export const useConditionBuildersRule: LintRule = {
12
+ id: "GHA002",
13
+ severity: "warning",
14
+ category: "style",
15
+ description: "Use Expression helpers instead of raw ${{ }} strings in if conditions",
16
+
17
+ check(context: LintContext): LintDiagnostic[] {
18
+ const { sourceFile } = context;
19
+ const diagnostics: LintDiagnostic[] = [];
20
+
21
+ function visit(node: ts.Node): void {
22
+ if (
23
+ ts.isPropertyAssignment(node) &&
24
+ ts.isIdentifier(node.name) &&
25
+ node.name.text === "if" &&
26
+ ts.isStringLiteral(node.initializer) &&
27
+ node.initializer.text.includes("${{")
28
+ ) {
29
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
30
+ diagnostics.push({
31
+ file: sourceFile.fileName,
32
+ line: line + 1,
33
+ column: character + 1,
34
+ ruleId: "GHA002",
35
+ severity: "warning",
36
+ message: "Use typed Expression helpers (e.g., github.ref.eq('refs/heads/main')) instead of raw ${{ }} strings.",
37
+ });
38
+ }
39
+ ts.forEachChild(node, visit);
40
+ }
41
+
42
+ visit(sourceFile);
43
+ return diagnostics;
44
+ },
45
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GHA004: Use Matrix Builder
3
+ *
4
+ * Flags inline object literals in `matrix` property. Suggests extracting
5
+ * to a named const for reusability and clarity.
6
+ */
7
+
8
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
+ import * as ts from "typescript";
10
+
11
+ export const useMatrixBuilderRule: LintRule = {
12
+ id: "GHA004",
13
+ severity: "info",
14
+ category: "style",
15
+ description: "Extract inline matrix objects to named constants",
16
+
17
+ check(context: LintContext): LintDiagnostic[] {
18
+ const { sourceFile } = context;
19
+ const diagnostics: LintDiagnostic[] = [];
20
+
21
+ function visit(node: ts.Node): void {
22
+ if (
23
+ ts.isPropertyAssignment(node) &&
24
+ ts.isIdentifier(node.name) &&
25
+ node.name.text === "matrix" &&
26
+ ts.isObjectLiteralExpression(node.initializer)
27
+ ) {
28
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
29
+ diagnostics.push({
30
+ file: sourceFile.fileName,
31
+ line: line + 1,
32
+ column: character + 1,
33
+ ruleId: "GHA004",
34
+ severity: "info",
35
+ message: "Consider extracting inline matrix to a named constant for clarity.",
36
+ });
37
+ }
38
+ ts.forEachChild(node, visit);
39
+ }
40
+
41
+ visit(sourceFile);
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * GHA001: Use Typed Action Composites
3
+ *
4
+ * Flags raw `uses:` string literals when a matching typed composite exists.
5
+ */
6
+
7
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
8
+ import * as ts from "typescript";
9
+ import { knownActions } from "./data/known-actions";
10
+
11
+ export const useTypedActionsRule: LintRule = {
12
+ id: "GHA001",
13
+ severity: "warning",
14
+ category: "style",
15
+ description: "Use typed action composite instead of raw uses: string",
16
+
17
+ check(context: LintContext): LintDiagnostic[] {
18
+ const { sourceFile } = context;
19
+ const diagnostics: LintDiagnostic[] = [];
20
+
21
+ function visit(node: ts.Node): void {
22
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "uses") {
23
+ if (ts.isStringLiteral(node.initializer)) {
24
+ const value = node.initializer.text;
25
+ // Extract action name without version: "actions/checkout@v4" → "actions/checkout"
26
+ const actionName = value.split("@")[0];
27
+ const match = knownActions[actionName];
28
+ if (match) {
29
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
30
+ diagnostics.push({
31
+ file: sourceFile.fileName,
32
+ line: line + 1,
33
+ column: character + 1,
34
+ ruleId: "GHA001",
35
+ severity: "warning",
36
+ message: `Use the typed ${match.composite}() composite instead of raw "${value}" string.`,
37
+ });
38
+ }
39
+ }
40
+ }
41
+ ts.forEachChild(node, visit);
42
+ }
43
+
44
+ visit(sourceFile);
45
+ return diagnostics;
46
+ },
47
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * GHA016: Validate Concurrency
3
+ *
4
+ * Flags `new Concurrency({cancelInProgress: true})` without `group`.
5
+ */
6
+
7
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
8
+ import * as ts from "typescript";
9
+
10
+ export const validateConcurrencyRule: LintRule = {
11
+ id: "GHA016",
12
+ severity: "warning",
13
+ category: "correctness",
14
+ description: "Concurrency with cancel-in-progress should specify a group",
15
+
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const { sourceFile } = context;
18
+ const diagnostics: LintDiagnostic[] = [];
19
+
20
+ function visit(node: ts.Node): void {
21
+ if (ts.isNewExpression(node)) {
22
+ let isConcurrency = false;
23
+ if (ts.isIdentifier(node.expression) && node.expression.text === "Concurrency") isConcurrency = true;
24
+ if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "Concurrency") isConcurrency = true;
25
+
26
+ if (isConcurrency && node.arguments?.length) {
27
+ const arg = node.arguments[0];
28
+ if (ts.isObjectLiteralExpression(arg)) {
29
+ let hasCancelInProgress = false;
30
+ let hasGroup = false;
31
+
32
+ for (const prop of arg.properties) {
33
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
34
+ if (prop.name.text === "cancelInProgress" || prop.name.text === "cancel-in-progress") {
35
+ // Check if it's set to true
36
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
37
+ hasCancelInProgress = true;
38
+ }
39
+ }
40
+ if (prop.name.text === "group") {
41
+ hasGroup = true;
42
+ }
43
+ }
44
+ }
45
+
46
+ if (hasCancelInProgress && !hasGroup) {
47
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
48
+ diagnostics.push({
49
+ file: sourceFile.fileName,
50
+ line: line + 1,
51
+ column: character + 1,
52
+ ruleId: "GHA016",
53
+ severity: "warning",
54
+ message: "Concurrency with cancel-in-progress should specify a group to avoid cancelling unrelated runs.",
55
+ });
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ts.forEachChild(node, visit);
61
+ }
62
+
63
+ visit(sourceFile);
64
+ return diagnostics;
65
+ },
66
+ };
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ describe("github completions", () => {
4
+ test("completions module exports githubCompletions", async () => {
5
+ const mod = await import("./completions");
6
+ expect(mod.githubCompletions).toBeDefined();
7
+ expect(typeof mod.githubCompletions).toBe("function");
8
+ });
9
+ });
@@ -0,0 +1,20 @@
1
+ import { createRequire } from "module";
2
+ import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ if (cachedIndex) return cachedIndex;
10
+ const data = require("../generated/lexicon-github.json") as Record<string, LexiconEntry>;
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+
15
+ /**
16
+ * Provide GitHub Actions completions based on context.
17
+ */
18
+ export function githubCompletions(ctx: CompletionContext): CompletionItem[] {
19
+ return lexiconCompletions(ctx, getIndex(), "GitHub Actions entity");
20
+ }
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ describe("github hover", () => {
4
+ test("hover module exports githubHover", async () => {
5
+ const mod = await import("./hover");
6
+ expect(mod.githubHover).toBeDefined();
7
+ expect(typeof mod.githubHover).toBe("function");
8
+ });
9
+ });
@@ -0,0 +1,38 @@
1
+ import { createRequire } from "module";
2
+ import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ if (cachedIndex) return cachedIndex;
10
+ const data = require("../generated/lexicon-github.json") as Record<string, LexiconEntry>;
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+
15
+ /**
16
+ * Provide hover information for GitHub Actions entity types.
17
+ */
18
+ export function githubHover(ctx: HoverContext): HoverInfo | undefined {
19
+ return lexiconHover(ctx, getIndex(), resourceHover);
20
+ }
21
+
22
+ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
23
+ const lines: string[] = [];
24
+
25
+ lines.push(`**${className}**`);
26
+ lines.push("");
27
+ lines.push(`GitHub Actions type: \`${entry.resourceType}\``);
28
+
29
+ if (entry.kind === "resource") {
30
+ lines.push("");
31
+ lines.push("*Resource entity — serialized as a top-level workflow/job entry*");
32
+ } else {
33
+ lines.push("");
34
+ lines.push("*Property entity — used as a nested value in resources*");
35
+ }
36
+
37
+ return { contents: lines.join("\n") };
38
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Thin entry point for `bun run bundle` in lexicon-github.
4
+ */
5
+ import { generate, writeGeneratedFiles } from "./codegen/generate";
6
+ import { packageLexicon } from "./codegen/package";
7
+ import { writeFileSync, mkdirSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
12
+
13
+ // 1. Generate src/generated/ files
14
+ const genResult = await generate({ verbose: true });
15
+ writeGeneratedFiles(genResult, pkgDir);
16
+ console.error(`Generated ${genResult.resources} entities, ${genResult.properties} property types, ${genResult.enums} enums`);
17
+
18
+ // 2. Run package pipeline and write dist/
19
+ const { spec, stats } = await packageLexicon({ verbose: true });
20
+
21
+ const distDir = join(pkgDir, "dist");
22
+ mkdirSync(join(distDir, "types"), { recursive: true });
23
+ mkdirSync(join(distDir, "rules"), { recursive: true });
24
+ mkdirSync(join(distDir, "skills"), { recursive: true });
25
+
26
+ writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
27
+ writeFileSync(join(distDir, "meta.json"), spec.registry);
28
+ writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
29
+
30
+ for (const [name, content] of spec.rules) {
31
+ writeFileSync(join(distDir, "rules", name), content);
32
+ }
33
+ for (const [name, content] of spec.skills) {
34
+ writeFileSync(join(distDir, "skills", name), content);
35
+ }
36
+
37
+ if (spec.integrity) {
38
+ writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
39
+ }
40
+
41
+ console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
42
+ console.error(`dist/ written to ${distDir}`);
@@ -0,0 +1,128 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { githubPlugin } from "./plugin";
3
+
4
+ describe("githubPlugin", () => {
5
+ test("has correct name", () => {
6
+ expect(githubPlugin.name).toBe("github");
7
+ });
8
+
9
+ test("has serializer", () => {
10
+ expect(githubPlugin.serializer).toBeDefined();
11
+ expect(githubPlugin.serializer.name).toBe("github");
12
+ });
13
+
14
+ test("provides lint rules", () => {
15
+ const rules = githubPlugin.lintRules!();
16
+ expect(rules.length).toBe(13);
17
+
18
+ const ruleIds = rules.map((r) => r.id);
19
+ expect(ruleIds).toContain("GHA001");
20
+ expect(ruleIds).toContain("GHA003");
21
+ expect(ruleIds).toContain("GHA014");
22
+ expect(ruleIds).toContain("GHA020");
23
+ });
24
+
25
+ test("provides post-synth checks", () => {
26
+ const checks = githubPlugin.postSynthChecks!();
27
+ expect(checks.length).toBe(6);
28
+
29
+ const checkIds = checks.map((c) => c.id);
30
+ expect(checkIds).toContain("GHA006");
31
+ expect(checkIds).toContain("GHA009");
32
+ expect(checkIds).toContain("GHA011");
33
+ expect(checkIds).toContain("GHA017");
34
+ expect(checkIds).toContain("GHA018");
35
+ expect(checkIds).toContain("GHA019");
36
+ });
37
+
38
+ test("provides intrinsics", () => {
39
+ const intrinsics = githubPlugin.intrinsics!();
40
+ expect(intrinsics.length).toBe(1);
41
+ expect(intrinsics[0].name).toBe("expression");
42
+ });
43
+
44
+ test("provides init templates", () => {
45
+ const defaultTemplate = githubPlugin.initTemplates!();
46
+ expect(defaultTemplate.src).toBeDefined();
47
+ expect(defaultTemplate.src["pipeline.ts"]).toContain("Workflow");
48
+ });
49
+
50
+ test("provides node-ci template", () => {
51
+ const template = githubPlugin.initTemplates!("node-ci");
52
+ expect(template.src["pipeline.ts"]).toContain("NodeCI");
53
+ });
54
+
55
+ test("provides docker-build template", () => {
56
+ const template = githubPlugin.initTemplates!("docker-build");
57
+ expect(template.src["pipeline.ts"]).toContain("docker");
58
+ });
59
+
60
+ test("detects GitHub Actions template", () => {
61
+ expect(githubPlugin.detectTemplate!({ on: {}, jobs: {} })).toBe(true);
62
+ });
63
+
64
+ test("detects job-like entries", () => {
65
+ expect(githubPlugin.detectTemplate!({
66
+ build: { "runs-on": "ubuntu-latest", steps: [] },
67
+ })).toBe(true);
68
+ });
69
+
70
+ test("rejects non-GitHub Actions data", () => {
71
+ expect(githubPlugin.detectTemplate!({ stages: [], image: "node:20" })).toBe(false);
72
+ });
73
+
74
+ test("provides skills", () => {
75
+ const skills = githubPlugin.skills!();
76
+ expect(skills.length).toBeGreaterThanOrEqual(1);
77
+ expect(skills[0].name).toBe("chant-github");
78
+ });
79
+
80
+ test("provides MCP tools", () => {
81
+ const tools = githubPlugin.mcpTools!();
82
+ expect(tools.length).toBe(1);
83
+ expect(tools[0].name).toBe("diff");
84
+ });
85
+
86
+ test("provides MCP resources", () => {
87
+ const resources = githubPlugin.mcpResources!();
88
+ expect(resources.length).toBe(2);
89
+ expect(resources[0].uri).toBe("resource-catalog");
90
+ expect(resources[1].uri).toBe("examples/basic-ci");
91
+ });
92
+
93
+ test("has completionProvider", () => {
94
+ expect(githubPlugin.completionProvider).toBeDefined();
95
+ });
96
+
97
+ test("has hoverProvider", () => {
98
+ expect(githubPlugin.hoverProvider).toBeDefined();
99
+ });
100
+
101
+ test("has templateParser", () => {
102
+ expect(githubPlugin.templateParser).toBeDefined();
103
+ });
104
+
105
+ test("has templateGenerator", () => {
106
+ expect(githubPlugin.templateGenerator).toBeDefined();
107
+ });
108
+
109
+ test("has generate method", () => {
110
+ expect(githubPlugin.generate).toBeDefined();
111
+ });
112
+
113
+ test("has validate method", () => {
114
+ expect(githubPlugin.validate).toBeDefined();
115
+ });
116
+
117
+ test("has coverage method", () => {
118
+ expect(githubPlugin.coverage).toBeDefined();
119
+ });
120
+
121
+ test("has package method", () => {
122
+ expect(githubPlugin.package).toBeDefined();
123
+ });
124
+
125
+ test("has docs method", () => {
126
+ expect(githubPlugin.docs).toBeDefined();
127
+ });
128
+ });