@intentius/chant-lexicon-github 0.1.23 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/integrity.json +34 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/{gha020.ts → gha013.ts} +4 -4
- package/dist/rules/gha029.ts +68 -0
- package/dist/rules/gha030.ts +41 -0
- package/dist/rules/gha031.ts +77 -0
- package/dist/rules/gha032.ts +39 -0
- package/dist/rules/gha033.ts +47 -0
- package/dist/rules/gha034.ts +41 -0
- package/dist/rules/gha035.ts +66 -0
- package/dist/rules/gha036.ts +42 -0
- package/dist/rules/gha037.ts +46 -0
- package/dist/rules/gha038.ts +41 -0
- package/dist/rules/gha039.ts +41 -0
- package/dist/rules/gha040.ts +43 -0
- package/dist/rules/gha041.ts +38 -0
- package/dist/rules/gha042.ts +40 -0
- package/dist/rules/gha043.ts +41 -0
- package/dist/rules/gha044.ts +41 -0
- package/dist/rules/gha045.ts +38 -0
- package/dist/rules/gha046.ts +56 -0
- package/dist/rules/gha047.ts +51 -0
- package/dist/rules/gha048.ts +41 -0
- package/dist/rules/gha049.ts +42 -0
- package/dist/rules/gha050.ts +44 -0
- package/dist/rules/gha051.ts +45 -0
- package/dist/rules/gha052.ts +42 -0
- package/dist/rules/gha053.ts +41 -0
- package/dist/rules/gha054.ts +41 -0
- package/dist/rules/gha055.ts +50 -0
- package/dist/rules/gha056.ts +37 -0
- package/dist/rules/gha057.ts +40 -0
- package/dist/rules/gha058.ts +48 -0
- package/dist/rules/yaml-helpers.ts +335 -0
- package/package.json +1 -1
- package/src/codegen/docs.ts +273 -4
- package/src/composites/dependabot.test.ts +48 -0
- package/src/composites/dependabot.ts +91 -0
- package/src/composites/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/lint/post-synth/{gha020.test.ts → gha013.test.ts} +7 -7
- package/src/lint/post-synth/{gha020.ts → gha013.ts} +4 -4
- package/src/lint/post-synth/gha029.test.ts +108 -0
- package/src/lint/post-synth/gha029.ts +68 -0
- package/src/lint/post-synth/gha030.test.ts +87 -0
- package/src/lint/post-synth/gha030.ts +41 -0
- package/src/lint/post-synth/gha031.test.ts +75 -0
- package/src/lint/post-synth/gha031.ts +77 -0
- package/src/lint/post-synth/gha032.test.ts +65 -0
- package/src/lint/post-synth/gha032.ts +39 -0
- package/src/lint/post-synth/gha033.test.ts +68 -0
- package/src/lint/post-synth/gha033.ts +47 -0
- package/src/lint/post-synth/gha034.test.ts +86 -0
- package/src/lint/post-synth/gha034.ts +41 -0
- package/src/lint/post-synth/gha035.test.ts +85 -0
- package/src/lint/post-synth/gha035.ts +66 -0
- package/src/lint/post-synth/gha036.test.ts +68 -0
- package/src/lint/post-synth/gha036.ts +42 -0
- package/src/lint/post-synth/gha037.test.ts +66 -0
- package/src/lint/post-synth/gha037.ts +46 -0
- package/src/lint/post-synth/gha038.test.ts +64 -0
- package/src/lint/post-synth/gha038.ts +41 -0
- package/src/lint/post-synth/gha039.test.ts +66 -0
- package/src/lint/post-synth/gha039.ts +41 -0
- package/src/lint/post-synth/gha040.test.ts +77 -0
- package/src/lint/post-synth/gha040.ts +43 -0
- package/src/lint/post-synth/gha041.test.ts +48 -0
- package/src/lint/post-synth/gha041.ts +38 -0
- package/src/lint/post-synth/gha042.test.ts +53 -0
- package/src/lint/post-synth/gha042.ts +40 -0
- package/src/lint/post-synth/gha043.test.ts +69 -0
- package/src/lint/post-synth/gha043.ts +41 -0
- package/src/lint/post-synth/gha044.test.ts +56 -0
- package/src/lint/post-synth/gha044.ts +41 -0
- package/src/lint/post-synth/gha045.test.ts +51 -0
- package/src/lint/post-synth/gha045.ts +38 -0
- package/src/lint/post-synth/gha046.test.ts +59 -0
- package/src/lint/post-synth/gha046.ts +56 -0
- package/src/lint/post-synth/gha047.test.ts +66 -0
- package/src/lint/post-synth/gha047.ts +51 -0
- package/src/lint/post-synth/gha048.test.ts +51 -0
- package/src/lint/post-synth/gha048.ts +41 -0
- package/src/lint/post-synth/gha049.test.ts +72 -0
- package/src/lint/post-synth/gha049.ts +42 -0
- package/src/lint/post-synth/gha050.test.ts +55 -0
- package/src/lint/post-synth/gha050.ts +44 -0
- package/src/lint/post-synth/gha051.test.ts +72 -0
- package/src/lint/post-synth/gha051.ts +45 -0
- package/src/lint/post-synth/gha052.test.ts +52 -0
- package/src/lint/post-synth/gha052.ts +42 -0
- package/src/lint/post-synth/gha053.test.ts +65 -0
- package/src/lint/post-synth/gha053.ts +41 -0
- package/src/lint/post-synth/gha054.test.ts +49 -0
- package/src/lint/post-synth/gha054.ts +41 -0
- package/src/lint/post-synth/gha055.test.ts +49 -0
- package/src/lint/post-synth/gha055.ts +50 -0
- package/src/lint/post-synth/gha056.test.ts +47 -0
- package/src/lint/post-synth/gha056.ts +37 -0
- package/src/lint/post-synth/gha057.test.ts +61 -0
- package/src/lint/post-synth/gha057.ts +40 -0
- package/src/lint/post-synth/gha058.test.ts +67 -0
- package/src/lint/post-synth/gha058.ts +48 -0
- package/src/lint/post-synth/yaml-helpers.ts +335 -0
- package/src/lint/rules/data/flagged-actions.ts +36 -0
- package/src/lint/rules/data/known-action-slugs.ts +38 -0
- package/src/lint/rules/data/preinstalled-tools.ts +36 -0
- package/src/lint/rules/data/risky-features.ts +39 -0
- package/src/lint/rules/data/trusted-action-owners.ts +12 -0
- package/src/lint/rules/data/untrusted-contexts.ts +44 -0
- package/src/mcp/context-tools.test.ts +108 -0
- package/src/mcp/context-tools.ts +172 -0
- package/src/plugin.test.ts +58 -3
- package/src/plugin.ts +5 -1
- package/src/serializer.ts +38 -5
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only MCP tools that expose what `chant build` already computes about a
|
|
3
|
+
* GitHub Actions workflow — its jobs and run order, what it pulls in from
|
|
4
|
+
* outside (actions, images), and its security findings — so an agent can ask
|
|
5
|
+
* about a change *before it runs or merges* (#327).
|
|
6
|
+
*
|
|
7
|
+
* The GitHub counterpart to the GitLab context tools. Every tool builds from
|
|
8
|
+
* source and returns data. None of them touch the live GitHub instance, read
|
|
9
|
+
* run history, or write anything — that boundary is what keeps chant a context
|
|
10
|
+
* producer and not a copy of the live tooling.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { build, type BuildResult } from "@intentius/chant/build";
|
|
14
|
+
import { runPostSynthChecks, getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
15
|
+
import { discoverPostSynthChecks } from "@intentius/chant/lint/discover";
|
|
16
|
+
import type { McpToolContribution } from "@intentius/chant/mcp/types";
|
|
17
|
+
import { dirname, join } from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { githubSerializer } from "../serializer";
|
|
20
|
+
import {
|
|
21
|
+
extractJobs,
|
|
22
|
+
extractActionRefs,
|
|
23
|
+
extractImageRefs,
|
|
24
|
+
extractTriggers,
|
|
25
|
+
extractWorkflowName,
|
|
26
|
+
} from "../lint/post-synth/yaml-helpers";
|
|
27
|
+
|
|
28
|
+
/** Build the project and return the emitted GitHub workflow YAML plus the raw result. */
|
|
29
|
+
async function buildGithub(path: string): Promise<{ yaml: string; result: BuildResult }> {
|
|
30
|
+
const result = await build(path, [githubSerializer]);
|
|
31
|
+
const out = result.outputs.get("github");
|
|
32
|
+
return { yaml: out ? getPrimaryOutput(out) : "", result };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Discover the lexicon's post-synth checks without depending on the plugin. */
|
|
36
|
+
function githubPostSynthChecks() {
|
|
37
|
+
const dir = join(dirname(fileURLToPath(import.meta.url)), "..", "lint", "post-synth");
|
|
38
|
+
return discoverPostSynthChecks(dir, import.meta.url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Is a `uses:` ref pinned to a full commit SHA (the only immutable form)? */
|
|
42
|
+
export function actionPinned(ref: string): boolean {
|
|
43
|
+
const at = ref.lastIndexOf("@");
|
|
44
|
+
return at !== -1 && /^[0-9a-f]{40}$/.test(ref.slice(at + 1));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Jobs that would re-run because they depend (transitively) on `job`. */
|
|
48
|
+
export function downstreamJobs(yaml: string, job: string): string[] {
|
|
49
|
+
const jobs = extractJobs(yaml);
|
|
50
|
+
// edge: upstream -> downstream (J depends on each of its needs)
|
|
51
|
+
const downstreamOf = new Map<string, string[]>();
|
|
52
|
+
for (const [name, j] of jobs) {
|
|
53
|
+
for (const dep of j.needs ?? []) {
|
|
54
|
+
const arr = downstreamOf.get(dep) ?? [];
|
|
55
|
+
arr.push(name);
|
|
56
|
+
downstreamOf.set(dep, arr);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const out = new Set<string>();
|
|
60
|
+
const queue = [job];
|
|
61
|
+
while (queue.length) {
|
|
62
|
+
const cur = queue.shift()!;
|
|
63
|
+
for (const next of downstreamOf.get(cur) ?? []) {
|
|
64
|
+
if (!out.has(next)) {
|
|
65
|
+
out.add(next);
|
|
66
|
+
queue.push(next);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...out];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PATH_INPUT = {
|
|
74
|
+
type: "object" as const,
|
|
75
|
+
properties: {
|
|
76
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The read-only context tools for the GitHub lexicon. */
|
|
81
|
+
export function githubContextTools(): McpToolContribution[] {
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
name: "github:checks",
|
|
85
|
+
description:
|
|
86
|
+
"Build the workflow and return its security and correctness findings (the GHA checks) as JSON. Read-only — does not touch the live GitHub instance.",
|
|
87
|
+
inputSchema: PATH_INPUT,
|
|
88
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
89
|
+
const { yaml, result } = await buildGithub((params.path as string) ?? ".");
|
|
90
|
+
if (!yaml) return { findings: [], note: "no GitHub workflow produced from this project" };
|
|
91
|
+
const scoped = { ...result, outputs: new Map([["github", result.outputs.get("github")!]]) };
|
|
92
|
+
const diags = runPostSynthChecks(githubPostSynthChecks(), scoped);
|
|
93
|
+
return {
|
|
94
|
+
findings: diags.map((d) => ({
|
|
95
|
+
id: d.checkId,
|
|
96
|
+
severity: d.severity,
|
|
97
|
+
job: d.entity ?? null,
|
|
98
|
+
message: d.message,
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "github:workflow",
|
|
105
|
+
description:
|
|
106
|
+
"Build the workflow and return its triggers and jobs as written (name, what each job runs after, step count) — before anything runs. Read-only.",
|
|
107
|
+
inputSchema: PATH_INPUT,
|
|
108
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
109
|
+
const { yaml } = await buildGithub((params.path as string) ?? ".");
|
|
110
|
+
const jobs = [...extractJobs(yaml).values()].map((j) => ({
|
|
111
|
+
name: j.name,
|
|
112
|
+
runsAfter: j.needs ?? [],
|
|
113
|
+
steps: j.steps?.length ?? 0,
|
|
114
|
+
}));
|
|
115
|
+
return {
|
|
116
|
+
workflow: extractWorkflowName(yaml) ?? null,
|
|
117
|
+
triggers: Object.keys(extractTriggers(yaml)),
|
|
118
|
+
jobs,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "github:references",
|
|
124
|
+
description:
|
|
125
|
+
"Build the workflow and list everything it pulls in from outside (actions via uses:, container/service images) and whether each is pinned to an immutable commit SHA. Read-only.",
|
|
126
|
+
inputSchema: PATH_INPUT,
|
|
127
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
128
|
+
const { yaml } = await buildGithub((params.path as string) ?? ".");
|
|
129
|
+
const actions = extractActionRefs(yaml).map((a) => ({
|
|
130
|
+
kind: a.level === "job" ? ("reusable-workflow" as const) : ("action" as const),
|
|
131
|
+
job: a.job,
|
|
132
|
+
source: a.ref,
|
|
133
|
+
pinned: actionPinned(a.ref),
|
|
134
|
+
}));
|
|
135
|
+
const images = extractImageRefs(yaml).map((i) => ({
|
|
136
|
+
kind: "image" as const,
|
|
137
|
+
job: i.job,
|
|
138
|
+
source: i.image,
|
|
139
|
+
pinned: i.image.includes("@sha256:"),
|
|
140
|
+
}));
|
|
141
|
+
return [...actions, ...images];
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "github:affected",
|
|
146
|
+
description:
|
|
147
|
+
"Build the workflow and, given a job name, list the jobs that would re-run because they depend on it (the needs chain). Read-only.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object" as const,
|
|
150
|
+
properties: {
|
|
151
|
+
path: { type: "string", description: "Path to the chant project directory (default: current directory)" },
|
|
152
|
+
job: { type: "string", description: "Job name to trace downstream from" },
|
|
153
|
+
},
|
|
154
|
+
required: ["job"],
|
|
155
|
+
},
|
|
156
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
157
|
+
const { yaml } = await buildGithub((params.path as string) ?? ".");
|
|
158
|
+
const job = params.job as string;
|
|
159
|
+
return { job, wouldRerun: downstreamJobs(yaml, job) };
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "github:workflow-yaml",
|
|
164
|
+
description: "Build the project and return the generated GitHub Actions workflow YAML as a string. Read-only.",
|
|
165
|
+
inputSchema: PATH_INPUT,
|
|
166
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
167
|
+
const { yaml } = await buildGithub((params.path as string) ?? ".");
|
|
168
|
+
return { yaml };
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
package/src/plugin.test.ts
CHANGED
|
@@ -24,15 +24,61 @@ describe("githubPlugin", () => {
|
|
|
24
24
|
|
|
25
25
|
test("provides post-synth checks", () => {
|
|
26
26
|
const checks = githubPlugin.postSynthChecks!();
|
|
27
|
-
expect(checks.length).toBe(
|
|
27
|
+
expect(checks.length).toBe(45);
|
|
28
28
|
|
|
29
29
|
const checkIds = checks.map((c) => c.id);
|
|
30
30
|
expect(checkIds).toContain("GHA006");
|
|
31
31
|
expect(checkIds).toContain("GHA009");
|
|
32
32
|
expect(checkIds).toContain("GHA011");
|
|
33
|
+
expect(checkIds).toContain("GHA013");
|
|
33
34
|
expect(checkIds).toContain("GHA017");
|
|
34
35
|
expect(checkIds).toContain("GHA018");
|
|
35
36
|
expect(checkIds).toContain("GHA019");
|
|
37
|
+
expect(checkIds).toContain("GHA029");
|
|
38
|
+
expect(checkIds).toContain("GHA030");
|
|
39
|
+
expect(checkIds).toContain("GHA031");
|
|
40
|
+
expect(checkIds).toContain("GHA032");
|
|
41
|
+
expect(checkIds).toContain("GHA033");
|
|
42
|
+
expect(checkIds).toContain("GHA034");
|
|
43
|
+
expect(checkIds).toContain("GHA035");
|
|
44
|
+
expect(checkIds).toContain("GHA036");
|
|
45
|
+
expect(checkIds).toContain("GHA037");
|
|
46
|
+
expect(checkIds).toContain("GHA038");
|
|
47
|
+
expect(checkIds).toContain("GHA039");
|
|
48
|
+
expect(checkIds).toContain("GHA040");
|
|
49
|
+
expect(checkIds).toContain("GHA041");
|
|
50
|
+
expect(checkIds).toContain("GHA042");
|
|
51
|
+
expect(checkIds).toContain("GHA043");
|
|
52
|
+
expect(checkIds).toContain("GHA044");
|
|
53
|
+
expect(checkIds).toContain("GHA045");
|
|
54
|
+
expect(checkIds).toContain("GHA046");
|
|
55
|
+
expect(checkIds).toContain("GHA047");
|
|
56
|
+
expect(checkIds).toContain("GHA048");
|
|
57
|
+
expect(checkIds).toContain("GHA049");
|
|
58
|
+
expect(checkIds).toContain("GHA050");
|
|
59
|
+
expect(checkIds).toContain("GHA051");
|
|
60
|
+
expect(checkIds).toContain("GHA052");
|
|
61
|
+
expect(checkIds).toContain("GHA053");
|
|
62
|
+
expect(checkIds).toContain("GHA054");
|
|
63
|
+
expect(checkIds).toContain("GHA055");
|
|
64
|
+
expect(checkIds).toContain("GHA056");
|
|
65
|
+
expect(checkIds).toContain("GHA057");
|
|
66
|
+
expect(checkIds).toContain("GHA058");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("lint IDs are unique across rules and post-synth checks", () => {
|
|
70
|
+
const ruleIds = githubPlugin.lintRules!().map((r) => r.id);
|
|
71
|
+
const checkIds = githubPlugin.postSynthChecks!().map((c) => c.id);
|
|
72
|
+
const allIds = [...ruleIds, ...checkIds];
|
|
73
|
+
|
|
74
|
+
const seen = new Set<string>();
|
|
75
|
+
const duplicates = new Set<string>();
|
|
76
|
+
for (const id of allIds) {
|
|
77
|
+
if (seen.has(id)) duplicates.add(id);
|
|
78
|
+
seen.add(id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expect(Array.from(duplicates)).toEqual([]);
|
|
36
82
|
});
|
|
37
83
|
|
|
38
84
|
test("provides intrinsics", () => {
|
|
@@ -79,8 +125,17 @@ describe("githubPlugin", () => {
|
|
|
79
125
|
|
|
80
126
|
test("provides MCP tools", () => {
|
|
81
127
|
const tools = githubPlugin.mcpTools!();
|
|
82
|
-
|
|
83
|
-
expect(
|
|
128
|
+
const names = tools.map((t) => t.name);
|
|
129
|
+
expect(names).toContain("github:diff");
|
|
130
|
+
// Read-only context tools (#327)
|
|
131
|
+
expect(names).toContain("github:checks");
|
|
132
|
+
expect(names).toContain("github:workflow");
|
|
133
|
+
expect(names).toContain("github:references");
|
|
134
|
+
expect(names).toContain("github:affected");
|
|
135
|
+
expect(names).toContain("github:workflow-yaml");
|
|
136
|
+
for (const t of tools) {
|
|
137
|
+
expect(typeof t.handler).toBe("function");
|
|
138
|
+
}
|
|
84
139
|
});
|
|
85
140
|
|
|
86
141
|
test("provides MCP resources", () => {
|
package/src/plugin.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { githubCompletions } from "./lsp/completions";
|
|
|
29
29
|
import { githubHover } from "./lsp/hover";
|
|
30
30
|
import { GitHubActionsParser } from "./import/parser";
|
|
31
31
|
import { GitHubActionsGenerator } from "./import/generator";
|
|
32
|
+
import { githubContextTools } from "./mcp/context-tools";
|
|
32
33
|
|
|
33
34
|
export const githubPlugin: LexiconPlugin = {
|
|
34
35
|
name: "github",
|
|
@@ -224,7 +225,10 @@ export const build = new Job({
|
|
|
224
225
|
},
|
|
225
226
|
|
|
226
227
|
mcpTools() {
|
|
227
|
-
return [
|
|
228
|
+
return [
|
|
229
|
+
createDiffTool(githubSerializer, "Compare current build output against previous output for GitHub Actions", "github"),
|
|
230
|
+
...githubContextTools(),
|
|
231
|
+
];
|
|
228
232
|
},
|
|
229
233
|
|
|
230
234
|
mcpResources() {
|
package/src/serializer.ts
CHANGED
|
@@ -233,6 +233,7 @@ export const githubSerializer: Serializer = {
|
|
|
233
233
|
const workflows: Array<[string, Declarable]> = [];
|
|
234
234
|
const jobs: Array<[string, Declarable]> = [];
|
|
235
235
|
const triggers: Array<[string, Declarable]> = [];
|
|
236
|
+
const dependabot: Array<[string, Declarable]> = [];
|
|
236
237
|
const others: Array<[string, Declarable]> = [];
|
|
237
238
|
|
|
238
239
|
for (const [name, entity] of entities) {
|
|
@@ -245,21 +246,53 @@ export const githubSerializer: Serializer = {
|
|
|
245
246
|
jobs.push([name, entity]);
|
|
246
247
|
} else if (isTriggerType(entityType)) {
|
|
247
248
|
triggers.push([name, entity]);
|
|
249
|
+
} else if (entityType === "GitHub::Dependabot::Config") {
|
|
250
|
+
dependabot.push([name, entity]);
|
|
248
251
|
} else {
|
|
249
252
|
others.push([name, entity]);
|
|
250
253
|
}
|
|
251
254
|
}
|
|
252
255
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
const workflowOut =
|
|
257
|
+
workflows.length > 1
|
|
258
|
+
? serializeMultiWorkflow(workflows, jobs, triggers, entities, entityNames)
|
|
259
|
+
: serializeSingleWorkflow(workflows, jobs, triggers, entities, entityNames);
|
|
260
|
+
|
|
261
|
+
// A DependabotConfig emits an additional `.github/dependabot.yml` file
|
|
262
|
+
// alongside (or instead of) any workflow output.
|
|
263
|
+
if (dependabot.length > 0) {
|
|
264
|
+
const [, dep] = dependabot[0];
|
|
265
|
+
const depProps = (dep as unknown as Record<string, unknown>).props as Record<string, unknown>;
|
|
266
|
+
const dependabotYaml = emitDependabotYaml(depProps);
|
|
267
|
+
if (typeof workflowOut === "string") {
|
|
268
|
+
return { primary: workflowOut, files: { "dependabot.yml": dependabotYaml } };
|
|
269
|
+
}
|
|
270
|
+
return { ...workflowOut, files: { ...(workflowOut.files ?? {}), "dependabot.yml": dependabotYaml } };
|
|
256
271
|
}
|
|
257
272
|
|
|
258
|
-
|
|
259
|
-
return serializeSingleWorkflow(workflows, jobs, triggers, entities, entityNames);
|
|
273
|
+
return workflowOut;
|
|
260
274
|
},
|
|
261
275
|
};
|
|
262
276
|
|
|
277
|
+
/** Deep camelCase → kebab-case key conversion for dependabot props. */
|
|
278
|
+
function deepKebabKeys(value: unknown): unknown {
|
|
279
|
+
if (Array.isArray(value)) return value.map(deepKebabKeys);
|
|
280
|
+
if (value !== null && typeof value === "object") {
|
|
281
|
+
const out: Record<string, unknown> = {};
|
|
282
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
283
|
+
out[toKebabCase(k)] = deepKebabKeys(v);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Serialize a DependabotConfig's props into `.github/dependabot.yml`. */
|
|
291
|
+
function emitDependabotYaml(props: Record<string, unknown>): string {
|
|
292
|
+
const doc = deepKebabKeys(props) as Record<string, unknown>;
|
|
293
|
+
return emitYAMLDocument(doc);
|
|
294
|
+
}
|
|
295
|
+
|
|
263
296
|
function serializeSingleWorkflow(
|
|
264
297
|
workflows: Array<[string, Declarable]>,
|
|
265
298
|
jobs: Array<[string, Declarable]>,
|