@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,31 @@
1
+ {
2
+ "algorithm": "xxhash64",
3
+ "artifacts": {
4
+ "manifest.json": "199b64fff9617c21",
5
+ "meta.json": "317499a992c9c274",
6
+ "types/index.d.ts": "93ce391baebf2afb",
7
+ "rules/missing-recommended-inputs.ts": "42f2f3b0b6c7b52c",
8
+ "rules/no-raw-expressions.ts": "6359f4d3135ed351",
9
+ "rules/use-typed-actions.ts": "eeedcc6145b7a132",
10
+ "rules/extract-inline-structs.ts": "646dce2eccf1fab4",
11
+ "rules/file-job-limit.ts": "7c46a302f6ba2744",
12
+ "rules/detect-secrets.ts": "999e6c5b4e048764",
13
+ "rules/deprecated-action-version.ts": "9ec91b190557f25f",
14
+ "rules/no-hardcoded-secrets.ts": "adcdb23f0480a4b7",
15
+ "rules/job-timeout.ts": "68f85c741c3d0ae8",
16
+ "rules/use-condition-builders.ts": "7406215df1f79fb8",
17
+ "rules/suggest-cache.ts": "c45f7659afde2f15",
18
+ "rules/validate-concurrency.ts": "c12a1aa4ee8badb5",
19
+ "rules/use-matrix-builder.ts": "6b1c0ebf43378805",
20
+ "rules/gha017.ts": "ff1c08fdedf83afa",
21
+ "rules/gha019.ts": "d9184093f36ac167",
22
+ "rules/gha009.ts": "df140c0cac573bc4",
23
+ "rules/gha018.ts": "46acbe27d4c0c817",
24
+ "rules/yaml-helpers.ts": "df426df288c175c9",
25
+ "rules/gha011.ts": "105e2d4faeaa9977",
26
+ "rules/gha006.ts": "baca27402ba18d",
27
+ "skills/chant-github.md": "e38b1aca37ae4ab8",
28
+ "skills/github-actions-patterns.md": "887ff05cbb3af292"
29
+ },
30
+ "composite": "e2321ecad63df490"
31
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "github",
3
+ "version": "0.0.18",
4
+ "chantVersion": ">=0.1.0",
5
+ "namespace": "GitHub",
6
+ "intrinsics": [
7
+ {
8
+ "name": "expression",
9
+ "description": "${{ }} expression wrapper for GitHub Actions contexts",
10
+ "outputKey": "expression",
11
+ "isTag": false
12
+ }
13
+ ],
14
+ "pseudoParameters": {}
15
+ }
package/dist/meta.json ADDED
@@ -0,0 +1,135 @@
1
+ {
2
+ "Concurrency": {
3
+ "resourceType": "GitHub::Actions::Concurrency",
4
+ "kind": "property",
5
+ "lexicon": "github"
6
+ },
7
+ "Container": {
8
+ "resourceType": "GitHub::Actions::Container",
9
+ "kind": "property",
10
+ "lexicon": "github"
11
+ },
12
+ "Defaults": {
13
+ "resourceType": "GitHub::Actions::Defaults",
14
+ "kind": "property",
15
+ "lexicon": "github"
16
+ },
17
+ "Environment": {
18
+ "resourceType": "GitHub::Actions::Environment",
19
+ "kind": "property",
20
+ "lexicon": "github"
21
+ },
22
+ "Job": {
23
+ "resourceType": "GitHub::Actions::Job",
24
+ "kind": "resource",
25
+ "lexicon": "github",
26
+ "constraints": {
27
+ "timeout-minutes": {
28
+ "default": 360
29
+ }
30
+ }
31
+ },
32
+ "Permissions": {
33
+ "resourceType": "GitHub::Actions::Permissions",
34
+ "kind": "property",
35
+ "lexicon": "github",
36
+ "constraints": {
37
+ "models": {
38
+ "enum": [
39
+ "read",
40
+ "none"
41
+ ]
42
+ }
43
+ }
44
+ },
45
+ "PullRequestTargetTrigger": {
46
+ "resourceType": "GitHub::Actions::PullRequestTargetTrigger",
47
+ "kind": "property",
48
+ "lexicon": "github"
49
+ },
50
+ "PullRequestTrigger": {
51
+ "resourceType": "GitHub::Actions::PullRequestTrigger",
52
+ "kind": "property",
53
+ "lexicon": "github"
54
+ },
55
+ "PushTrigger": {
56
+ "resourceType": "GitHub::Actions::PushTrigger",
57
+ "kind": "property",
58
+ "lexicon": "github"
59
+ },
60
+ "RepositoryDispatchTrigger": {
61
+ "resourceType": "GitHub::Actions::RepositoryDispatchTrigger",
62
+ "kind": "property",
63
+ "lexicon": "github"
64
+ },
65
+ "ReusableWorkflowCallJob": {
66
+ "resourceType": "GitHub::Actions::ReusableWorkflowCallJob",
67
+ "kind": "resource",
68
+ "lexicon": "github",
69
+ "constraints": {
70
+ "uses": {
71
+ "pattern": "^(.+\\/)+(.+)\\.(ya?ml)(@.+)?$"
72
+ }
73
+ }
74
+ },
75
+ "ScheduleTrigger": {
76
+ "resourceType": "GitHub::Actions::ScheduleTrigger",
77
+ "kind": "property",
78
+ "lexicon": "github"
79
+ },
80
+ "Service": {
81
+ "resourceType": "GitHub::Actions::Service",
82
+ "kind": "property",
83
+ "lexicon": "github"
84
+ },
85
+ "Step": {
86
+ "resourceType": "GitHub::Actions::Step",
87
+ "kind": "property",
88
+ "lexicon": "github"
89
+ },
90
+ "Strategy": {
91
+ "resourceType": "GitHub::Actions::Strategy",
92
+ "kind": "property",
93
+ "lexicon": "github",
94
+ "constraints": {
95
+ "fail-fast": {
96
+ "default": true
97
+ }
98
+ }
99
+ },
100
+ "Workflow": {
101
+ "resourceType": "GitHub::Actions::Workflow",
102
+ "kind": "resource",
103
+ "lexicon": "github"
104
+ },
105
+ "WorkflowCallTrigger": {
106
+ "resourceType": "GitHub::Actions::WorkflowCallTrigger",
107
+ "kind": "property",
108
+ "lexicon": "github"
109
+ },
110
+ "WorkflowDispatchTrigger": {
111
+ "resourceType": "GitHub::Actions::WorkflowDispatchTrigger",
112
+ "kind": "property",
113
+ "lexicon": "github"
114
+ },
115
+ "WorkflowInput": {
116
+ "resourceType": "GitHub::Actions::WorkflowInput",
117
+ "kind": "property",
118
+ "lexicon": "github"
119
+ },
120
+ "WorkflowOutput": {
121
+ "resourceType": "GitHub::Actions::WorkflowOutput",
122
+ "kind": "property",
123
+ "lexicon": "github"
124
+ },
125
+ "WorkflowRunTrigger": {
126
+ "resourceType": "GitHub::Actions::WorkflowRunTrigger",
127
+ "kind": "property",
128
+ "lexicon": "github"
129
+ },
130
+ "WorkflowSecret": {
131
+ "resourceType": "GitHub::Actions::WorkflowSecret",
132
+ "kind": "property",
133
+ "lexicon": "github"
134
+ }
135
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * GHA012: Deprecated Action Version
3
+ *
4
+ * Flags `uses:` strings referencing deprecated action versions.
5
+ */
6
+
7
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
8
+ import * as ts from "typescript";
9
+ import { deprecatedVersions } from "./data/deprecated-versions";
10
+
11
+ export const deprecatedActionVersionRule: LintRule = {
12
+ id: "GHA012",
13
+ severity: "warning",
14
+ category: "correctness",
15
+ description: "Action version is deprecated — upgrade to the recommended version",
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.isStringLiteral(node)) {
23
+ const text = node.text;
24
+ const atIndex = text.indexOf("@");
25
+ if (atIndex === -1) { ts.forEachChild(node, visit); return; }
26
+
27
+ const actionName = text.slice(0, atIndex);
28
+ const version = text.slice(atIndex + 1);
29
+ const info = deprecatedVersions[actionName];
30
+
31
+ if (info && info.deprecated.includes(version)) {
32
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
33
+ diagnostics.push({
34
+ file: sourceFile.fileName,
35
+ line: line + 1,
36
+ column: character + 1,
37
+ ruleId: "GHA012",
38
+ severity: "warning",
39
+ message: `"${text}" uses deprecated version ${version}. Upgrade to ${actionName}@${info.recommended}.`,
40
+ });
41
+ }
42
+ }
43
+ ts.forEachChild(node, visit);
44
+ }
45
+
46
+ visit(sourceFile);
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GHA020: Detect Secrets
3
+ *
4
+ * Scans string literals for patterns matching known secret formats.
5
+ * Skips strings containing "secrets." (proper usage).
6
+ */
7
+
8
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
9
+ import * as ts from "typescript";
10
+ import { secretPatterns } from "./data/secret-patterns";
11
+
12
+ export const detectSecretsRule: LintRule = {
13
+ id: "GHA020",
14
+ severity: "error",
15
+ category: "security",
16
+ description: "Potential secret detected in source code",
17
+
18
+ check(context: LintContext): LintDiagnostic[] {
19
+ const { sourceFile } = context;
20
+ const diagnostics: LintDiagnostic[] = [];
21
+
22
+ function visit(node: ts.Node): void {
23
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
24
+ const text = node.text;
25
+
26
+ // Skip strings that reference secrets properly
27
+ if (text.includes("secrets.")) {
28
+ ts.forEachChild(node, visit);
29
+ return;
30
+ }
31
+
32
+ for (const { pattern, description } of secretPatterns) {
33
+ if (pattern.test(text)) {
34
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
35
+ diagnostics.push({
36
+ file: sourceFile.fileName,
37
+ line: line + 1,
38
+ column: character + 1,
39
+ ruleId: "GHA020",
40
+ severity: "error",
41
+ message: `Potential ${description} detected. Use secrets() to reference secrets securely.`,
42
+ });
43
+ break; // One diagnostic per string
44
+ }
45
+ }
46
+ }
47
+ ts.forEachChild(node, visit);
48
+ }
49
+
50
+ visit(sourceFile);
51
+ return diagnostics;
52
+ },
53
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * GHA005: Extract Inline Structs
3
+ *
4
+ * Flags object literal nesting depth > 2 inside resource constructors.
5
+ */
6
+
7
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
8
+ import * as ts from "typescript";
9
+
10
+ const RESOURCE_NAMES = new Set(["Job", "Workflow", "ReusableWorkflowCallJob"]);
11
+ const MAX_DEPTH = 2;
12
+
13
+ export const extractInlineStructsRule: LintRule = {
14
+ id: "GHA005",
15
+ severity: "info",
16
+ category: "style",
17
+ description: "Extract deeply nested inline objects to named constants",
18
+
19
+ check(context: LintContext): LintDiagnostic[] {
20
+ const { sourceFile } = context;
21
+ const diagnostics: LintDiagnostic[] = [];
22
+
23
+ function isResourceConstructor(node: ts.NewExpression): boolean {
24
+ if (ts.isIdentifier(node.expression)) return RESOURCE_NAMES.has(node.expression.text);
25
+ if (ts.isPropertyAccessExpression(node.expression)) return RESOURCE_NAMES.has(node.expression.name.text);
26
+ return false;
27
+ }
28
+
29
+ function checkDepth(node: ts.ObjectLiteralExpression, depth: number): void {
30
+ if (depth > MAX_DEPTH) {
31
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
32
+ diagnostics.push({
33
+ file: sourceFile.fileName,
34
+ line: line + 1,
35
+ column: character + 1,
36
+ ruleId: "GHA005",
37
+ severity: "info",
38
+ message: `Object nesting depth ${depth} exceeds ${MAX_DEPTH}. Consider extracting to a named constant.`,
39
+ });
40
+ return;
41
+ }
42
+ for (const prop of node.properties) {
43
+ if (ts.isPropertyAssignment(prop) && ts.isObjectLiteralExpression(prop.initializer)) {
44
+ checkDepth(prop.initializer, depth + 1);
45
+ }
46
+ }
47
+ }
48
+
49
+ function visit(node: ts.Node): void {
50
+ if (ts.isNewExpression(node) && isResourceConstructor(node) && node.arguments?.[0]) {
51
+ const arg = node.arguments[0];
52
+ if (ts.isObjectLiteralExpression(arg)) {
53
+ checkDepth(arg, 1);
54
+ }
55
+ }
56
+ ts.forEachChild(node, visit);
57
+ }
58
+
59
+ visit(sourceFile);
60
+ return diagnostics;
61
+ },
62
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * GHA007: File Job Limit
3
+ *
4
+ * Flags files with more than 10 Job/ReusableWorkflowCallJob constructors.
5
+ */
6
+
7
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
8
+ import * as ts from "typescript";
9
+
10
+ const JOB_NAMES = new Set(["Job", "ReusableWorkflowCallJob"]);
11
+ const MAX_JOBS = 10;
12
+
13
+ export const fileJobLimitRule: LintRule = {
14
+ id: "GHA007",
15
+ severity: "warning",
16
+ category: "style",
17
+ description: "Too many jobs in a single file",
18
+
19
+ check(context: LintContext): LintDiagnostic[] {
20
+ const { sourceFile } = context;
21
+ const diagnostics: LintDiagnostic[] = [];
22
+ let jobCount = 0;
23
+
24
+ function visit(node: ts.Node): void {
25
+ if (ts.isNewExpression(node)) {
26
+ let isJob = false;
27
+ if (ts.isIdentifier(node.expression)) isJob = JOB_NAMES.has(node.expression.text);
28
+ if (ts.isPropertyAccessExpression(node.expression)) isJob = JOB_NAMES.has(node.expression.name.text);
29
+ if (isJob) jobCount++;
30
+ }
31
+ ts.forEachChild(node, visit);
32
+ }
33
+
34
+ visit(sourceFile);
35
+
36
+ if (jobCount > MAX_JOBS) {
37
+ diagnostics.push({
38
+ file: sourceFile.fileName,
39
+ line: 1,
40
+ column: 1,
41
+ ruleId: "GHA007",
42
+ severity: "warning",
43
+ message: `File has ${jobCount} job constructors (limit: ${MAX_JOBS}). Consider splitting into multiple files.`,
44
+ });
45
+ }
46
+
47
+ return diagnostics;
48
+ },
49
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * GHA006: Duplicate Workflow Name
3
+ *
4
+ * Detects multiple workflows sharing the same `name:` value.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { extractWorkflowName } from "./yaml-helpers";
9
+ import type { SerializerResult } from "@intentius/chant/serializer";
10
+
11
+ export const gha006: PostSynthCheck = {
12
+ id: "GHA006",
13
+ description: "Multiple workflows share the same name",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ const diagnostics: PostSynthDiagnostic[] = [];
17
+ const nameMap = new Map<string, string[]>();
18
+
19
+ for (const [outputName, output] of ctx.outputs) {
20
+ const yaml = typeof output === "string" ? output : (output as SerializerResult).primary;
21
+
22
+ // Check if this is a multi-file output
23
+ if (typeof output === "object" && "files" in output) {
24
+ const result = output as SerializerResult;
25
+ if (result.files) {
26
+ for (const [fileName, fileContent] of Object.entries(result.files)) {
27
+ const name = extractWorkflowName(fileContent);
28
+ if (name) {
29
+ const existing = nameMap.get(name) ?? [];
30
+ existing.push(fileName);
31
+ nameMap.set(name, existing);
32
+ }
33
+ }
34
+ }
35
+ } else {
36
+ const name = extractWorkflowName(yaml);
37
+ if (name) {
38
+ const existing = nameMap.get(name) ?? [];
39
+ existing.push(outputName);
40
+ nameMap.set(name, existing);
41
+ }
42
+ }
43
+ }
44
+
45
+ for (const [name, files] of nameMap) {
46
+ if (files.length > 1) {
47
+ diagnostics.push({
48
+ checkId: "GHA006",
49
+ severity: "error",
50
+ message: `Duplicate workflow name "${name}" found in: ${files.join(", ")}`,
51
+ lexicon: "github",
52
+ });
53
+ }
54
+ }
55
+
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * GHA009: Empty Matrix Dimension
3
+ *
4
+ * Detects matrix dimensions with empty values arrays.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { getPrimaryOutput } from "./yaml-helpers";
9
+
10
+ export const gha009: PostSynthCheck = {
11
+ id: "GHA009",
12
+ description: "Matrix dimension has empty values array",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ const diagnostics: PostSynthDiagnostic[] = [];
16
+
17
+ for (const [, output] of ctx.outputs) {
18
+ const yaml = getPrimaryOutput(output);
19
+
20
+ // Find matrix sections and check for empty arrays
21
+ const matrixMatch = yaml.match(/matrix:\n([\s\S]*?)(?=\n\s{4}[a-z]|\n\s{2}[a-z]|\n[a-z]|$)/gm);
22
+ if (!matrixMatch) continue;
23
+
24
+ for (const section of matrixMatch) {
25
+ const lines = section.split("\n");
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const keyMatch = lines[i].match(/^\s+([a-z][a-z0-9_-]*):\s*\[\s*\]\s*$/);
28
+ if (keyMatch) {
29
+ diagnostics.push({
30
+ checkId: "GHA009",
31
+ severity: "error",
32
+ message: `Matrix dimension "${keyMatch[1]}" has an empty values array.`,
33
+ lexicon: "github",
34
+ });
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ return diagnostics;
41
+ },
42
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GHA011: Invalid Needs Reference
3
+ *
4
+ * Detects `needs:` references to non-existent job IDs.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { getPrimaryOutput, extractJobs } from "./yaml-helpers";
9
+
10
+ export const gha011: PostSynthCheck = {
11
+ id: "GHA011",
12
+ description: "Job needs: references non-existent job",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ const diagnostics: PostSynthDiagnostic[] = [];
16
+
17
+ for (const [, output] of ctx.outputs) {
18
+ const yaml = getPrimaryOutput(output);
19
+ const jobs = extractJobs(yaml);
20
+ const jobNames = new Set(jobs.keys());
21
+
22
+ for (const [jobName, job] of jobs) {
23
+ if (!job.needs) continue;
24
+ for (const need of job.needs) {
25
+ if (!jobNames.has(need)) {
26
+ diagnostics.push({
27
+ checkId: "GHA011",
28
+ severity: "error",
29
+ message: `Job "${jobName}" needs "${need}", but no such job exists.`,
30
+ entity: jobName,
31
+ lexicon: "github",
32
+ });
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ return diagnostics;
39
+ },
40
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * GHA017: Missing Permissions
3
+ *
4
+ * Flags workflows without an explicit `permissions:` block.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { getPrimaryOutput, hasPermissions } from "./yaml-helpers";
9
+
10
+ export const gha017: PostSynthCheck = {
11
+ id: "GHA017",
12
+ description: "Workflow without explicit permissions block",
13
+
14
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ const diagnostics: PostSynthDiagnostic[] = [];
16
+
17
+ for (const [, output] of ctx.outputs) {
18
+ const yaml = getPrimaryOutput(output);
19
+
20
+ if (!hasPermissions(yaml)) {
21
+ diagnostics.push({
22
+ checkId: "GHA017",
23
+ severity: "info",
24
+ message: "Workflow does not specify permissions. Consider adding explicit permissions for least-privilege security.",
25
+ lexicon: "github",
26
+ });
27
+ }
28
+ }
29
+
30
+ return diagnostics;
31
+ },
32
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GHA018: Pull Request Target + Checkout Security Risk
3
+ *
4
+ * Flags workflows using `pull_request_target` trigger with a checkout action
5
+ * in steps — this is a known security anti-pattern.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { getPrimaryOutput, extractJobs, extractTriggers, hasCheckoutAction } from "./yaml-helpers";
10
+
11
+ export const gha018: PostSynthCheck = {
12
+ id: "GHA018",
13
+ description: "pull_request_target with checkout action is a security risk",
14
+
15
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
16
+ const diagnostics: PostSynthDiagnostic[] = [];
17
+
18
+ for (const [, output] of ctx.outputs) {
19
+ const yaml = getPrimaryOutput(output);
20
+ const triggers = extractTriggers(yaml);
21
+
22
+ if (!triggers["pull_request_target"]) continue;
23
+
24
+ const jobs = extractJobs(yaml);
25
+ for (const [jobName, job] of jobs) {
26
+ if (job.steps && hasCheckoutAction(job.steps)) {
27
+ diagnostics.push({
28
+ checkId: "GHA018",
29
+ severity: "warning",
30
+ message: `Job "${jobName}" uses checkout with pull_request_target trigger. This runs untrusted PR code with write permissions — a security risk.`,
31
+ entity: jobName,
32
+ lexicon: "github",
33
+ });
34
+ }
35
+ }
36
+ }
37
+
38
+ return diagnostics;
39
+ },
40
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * GHA019: Circular Needs Chain
3
+ *
4
+ * DFS-based cycle detection on the job `needs:` dependency graph.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { getPrimaryOutput, buildNeedsGraph } from "./yaml-helpers";
9
+
10
+ export function checkCircularNeeds(ctx: PostSynthContext): PostSynthDiagnostic[] {
11
+ const diagnostics: PostSynthDiagnostic[] = [];
12
+
13
+ for (const [, output] of ctx.outputs) {
14
+ const yaml = getPrimaryOutput(output);
15
+ const graph = buildNeedsGraph(yaml);
16
+
17
+ const visited = new Set<string>();
18
+ const inStack = new Set<string>();
19
+ const reportedInCycle = new Set<string>();
20
+
21
+ function dfs(node: string, path: string[]): void {
22
+ if (inStack.has(node)) {
23
+ const cycleStart = path.indexOf(node);
24
+ const cycle = path.slice(cycleStart);
25
+ cycle.push(node);
26
+
27
+ const cycleKey = [...cycle].sort().join(",");
28
+ if (!reportedInCycle.has(cycleKey)) {
29
+ reportedInCycle.add(cycleKey);
30
+ diagnostics.push({
31
+ checkId: "GHA019",
32
+ severity: "error",
33
+ message: `Circular needs: chain detected: ${cycle.join(" → ")}`,
34
+ entity: node,
35
+ lexicon: "github",
36
+ });
37
+ }
38
+ return;
39
+ }
40
+
41
+ if (visited.has(node)) return;
42
+
43
+ visited.add(node);
44
+ inStack.add(node);
45
+
46
+ for (const neighbor of graph.get(node) ?? []) {
47
+ if (graph.has(neighbor)) {
48
+ dfs(neighbor, [...path, node]);
49
+ }
50
+ }
51
+
52
+ inStack.delete(node);
53
+ }
54
+
55
+ for (const jobName of graph.keys()) {
56
+ if (!visited.has(jobName)) {
57
+ dfs(jobName, []);
58
+ }
59
+ }
60
+ }
61
+
62
+ return diagnostics;
63
+ }
64
+
65
+ export const gha019: PostSynthCheck = {
66
+ id: "GHA019",
67
+ description: "Circular needs: chain — cycle in job dependency graph",
68
+
69
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
70
+ return checkCircularNeeds(ctx);
71
+ },
72
+ };