@intentius/chant-lexicon-github 0.2.0 → 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 +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/src/codegen/docs.ts +85 -4
- package/src/mcp/context-tools.test.ts +108 -0
- package/src/mcp/context-tools.ts +172 -0
- package/src/plugin.test.ts +11 -2
- package/src/plugin.ts +5 -1
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "0fa34c5776ecd073eddf047bf545d4e7eec1b2bef82bda8e8b9a937dafefdbe3",
|
|
5
5
|
"meta.json": "798c5f5f37b0174756d2451299acfdc95c014f11d06d32b9153bbdfa9f3a580c",
|
|
6
6
|
"types/index.d.ts": "39c0791f0e58025bcef82e0f7a92240508f899fc2771d5dd51b2369b4d330f13",
|
|
7
7
|
"rules/deprecated-action-version.ts": "d41e6e532ab7f623af1bee4ac5279fcb2baada7defa1c5d022a5bc71983e8797",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"skills/chant-github-patterns.md": "bb3abef289a8fdfcf07d6bb2d7289dcb2f38bc0cb0321ea320b78b45a6f548c0",
|
|
68
68
|
"skills/chant-github-security.md": "aab111cb0871cad30281ce48d7da23663689619351029219e2be019a1a61e394"
|
|
69
69
|
},
|
|
70
|
-
"composite": "
|
|
70
|
+
"composite": "b9406b7d04e47eb1ac9b2562be29fad73787acf1a3d610c2920fd96d874f85f9"
|
|
71
71
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
package/src/codegen/docs.ts
CHANGED
|
@@ -25,7 +25,7 @@ npm install --save-dev @intentius/chant-lexicon-github
|
|
|
25
25
|
|
|
26
26
|
{{file:docs-snippets/src/quickstart.ts}}
|
|
27
27
|
|
|
28
|
-
The lexicon provides **
|
|
28
|
+
The lexicon provides **3 resources** (Workflow, Job, Dependabot config), **14 composites** (Checkout, SetupNode, SetupGo, SetupPython, CacheAction, UploadArtifact, DownloadArtifact, NodeCI, NodePipeline, PythonCI, DockerBuild, DeployEnvironment, GoCI, Dependabot) + **3 presets** (BunPipeline, PnpmPipeline, YarnPipeline), a typed **Expression** system with 24 GitHub and 5 Runner context variables, and **13 lint rules** + **45 post-synth checks** (including a CI/CD supply-chain security pass, GHA029–058).
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
31
|
const outputFormat = `The GitHub Actions lexicon serializes resources into **\`.github/workflows/*.yml\`** YAML files.
|
|
@@ -184,7 +184,7 @@ GitHub automatically picks up any \`.github/workflows/*.yml\` files and runs the
|
|
|
184
184
|
- [Workflow Concepts](/chant/lexicons/github/workflow-concepts/) — resource types, triggers, permissions
|
|
185
185
|
- [Expressions](/chant/lexicons/github/expressions/) — typed expression system and condition helpers
|
|
186
186
|
- [Composites](/chant/lexicons/github/composites/) — pre-built action wrappers (Checkout, SetupNode, etc.)
|
|
187
|
-
- [Lint Rules](/chant/lexicons/github/lint-rules/) — 13 lint rules and
|
|
187
|
+
- [Lint Rules](/chant/lexicons/github/lint-rules/) — 13 lint rules and 45 post-synth checks`,
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
slug: "workflow-concepts",
|
|
@@ -838,7 +838,27 @@ const { workflow, buildJob, testJob, lintJob } = GoCI({
|
|
|
838
838
|
const noLint = GoCI({ lintCommand: null });
|
|
839
839
|
\`\`\`
|
|
840
840
|
|
|
841
|
-
**Props:** \`goVersion?\`, \`testCommand?\`, \`buildCommand?\`, \`lintCommand?\` (null to omit), \`runsOn
|
|
841
|
+
**Props:** \`goVersion?\`, \`testCommand?\`, \`buildCommand?\`, \`lintCommand?\` (null to omit), \`runsOn?\`
|
|
842
|
+
|
|
843
|
+
## Dependabot
|
|
844
|
+
|
|
845
|
+
Models the repository's dependency-update configuration (\`.github/dependabot.yml\`) as a chant resource, so it is emitted and lintable like a workflow. The composite ships **safe defaults**: a cooldown window on every ecosystem (so a version published moments ago — including a compromised one — is not adopted before anyone can react) and external code execution explicitly denied.
|
|
846
|
+
|
|
847
|
+
\`\`\`typescript
|
|
848
|
+
import { Dependabot } from "@intentius/chant-lexicon-github";
|
|
849
|
+
|
|
850
|
+
export const dependabot = Dependabot({
|
|
851
|
+
ecosystems: [
|
|
852
|
+
{ packageEcosystem: "npm", directory: "/" },
|
|
853
|
+
{ packageEcosystem: "github-actions", directory: "/" },
|
|
854
|
+
],
|
|
855
|
+
// cooldownDays defaults to 7, openPullRequestsLimit to 5
|
|
856
|
+
});
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
**Props:** \`ecosystems\` (required — each \`{ packageEcosystem, directory?, interval? }\`), \`cooldownDays?\` (default 7), \`openPullRequestsLimit?\` (default 5)
|
|
860
|
+
|
|
861
|
+
For full control, construct the \`DependabotConfig\` resource directly with raw \`updates:\` entries. Two post-synth checks validate the emitted config: **GHA057** (\`insecure-external-code-execution: allow\`) and **GHA058** (no cooldown). See [Lint Rules](/chant/lexicons/github/lint-rules/).`,
|
|
842
862
|
},
|
|
843
863
|
{
|
|
844
864
|
slug: "lint-rules",
|
|
@@ -976,6 +996,60 @@ Flags workflows triggered by \`pull_request_target\` that include \`actions/chec
|
|
|
976
996
|
|
|
977
997
|
Detects cycles in the \`needs:\` dependency graph. If job A needs B and B needs A (directly or transitively), GitHub rejects the workflow. Reports the full cycle chain in the diagnostic message.
|
|
978
998
|
|
|
999
|
+
### GHA021 — Checkout action not pinned to a SHA
|
|
1000
|
+
|
|
1001
|
+
**Severity:** warning
|
|
1002
|
+
|
|
1003
|
+
Flags \`actions/checkout\` referenced by a tag (e.g. \`@v4\`) instead of a pinned commit SHA. The narrower precursor to GHA029, kept because checkout is the most common unpinned action.
|
|
1004
|
+
|
|
1005
|
+
### GHA022 — Job without timeout-minutes
|
|
1006
|
+
|
|
1007
|
+
**Severity:** info
|
|
1008
|
+
|
|
1009
|
+
Flags jobs that omit \`timeout-minutes\`. A hung step otherwise runs to the runner's default cap, burning minutes — set an explicit ceiling.
|
|
1010
|
+
|
|
1011
|
+
### GHA023 — Deprecated set-output command
|
|
1012
|
+
|
|
1013
|
+
**Severity:** warning
|
|
1014
|
+
|
|
1015
|
+
Flags \`::set-output\` in \`run:\` steps. The workflow command is deprecated and disabled on current runners — write to \`$GITHUB_OUTPUT\` instead.
|
|
1016
|
+
|
|
1017
|
+
### GHA024 — Missing concurrency for deploy workflows
|
|
1018
|
+
|
|
1019
|
+
**Severity:** info
|
|
1020
|
+
|
|
1021
|
+
Flags deploy workflows without a \`concurrency:\` block. Without one, two pushes can deploy concurrently and race — add a concurrency group to serialize them.
|
|
1022
|
+
|
|
1023
|
+
### GHA025 — pull_request_target without restrictions
|
|
1024
|
+
|
|
1025
|
+
**Severity:** warning
|
|
1026
|
+
|
|
1027
|
+
Flags \`pull_request_target\` used without a \`types:\` filter. The trigger runs with repository secrets in the base-branch context, so it should be scoped to the specific PR events that need it.
|
|
1028
|
+
|
|
1029
|
+
### GHA026 — Secret used without environment protection
|
|
1030
|
+
|
|
1031
|
+
**Severity:** info
|
|
1032
|
+
|
|
1033
|
+
Flags workflows that reference \`secrets.\` in steps but declare no \`environment:\` on any job, so the secret skips the approval and scoping rules an environment gate provides.
|
|
1034
|
+
|
|
1035
|
+
### GHA027 — Cleanup step missing if: always()
|
|
1036
|
+
|
|
1037
|
+
**Severity:** info
|
|
1038
|
+
|
|
1039
|
+
Flags steps named "cleanup" / "teardown" / "clean up" that lack an \`if:\` condition. Cleanup should run even when a prior step fails — add \`if: always()\`.
|
|
1040
|
+
|
|
1041
|
+
### GHA028 — Workflow with no on: triggers
|
|
1042
|
+
|
|
1043
|
+
**Severity:** error
|
|
1044
|
+
|
|
1045
|
+
Flags a workflow file with no top-level \`on:\` key. Without a trigger the workflow can never run.
|
|
1046
|
+
|
|
1047
|
+
## Supply-chain security pass (GHA029–058)
|
|
1048
|
+
|
|
1049
|
+
GHA029 onward are a CI/CD supply-chain security pass: pin & vet external references, enforce least-privilege token scopes, guard trust boundaries against untrusted input, contain secrets, reject unsound expressions, and keep artifacts/caches honest. They run statically on the emitted YAML — everything answerable without leaving the build.
|
|
1050
|
+
|
|
1051
|
+
The checks that need a *moving external truth* — whether a pinned SHA still maps to a real upstream tag, whether a ref still exists, whether a new advisory now covers an action in use — can't be deterministic, so they live in the operational layer instead. Schedule the [\`WorkflowAuditOp\`](/chant/guide/ops/#audit-supply-chain-drift) (temporal lexicon) for that live, always-fresh half; it reads the same emitted workflow references and reports drift via \`report | issue | pull-request\`.
|
|
1052
|
+
|
|
979
1053
|
### GHA029 — Action or reusable workflow not pinned to a commit SHA
|
|
980
1054
|
|
|
981
1055
|
**Severity:** warning
|
|
@@ -1287,7 +1361,7 @@ The inline \`chant-github\` skill covers the full workflow lifecycle:
|
|
|
1287
1361
|
- **Validate** — \`chant lint src/\`
|
|
1288
1362
|
- **Deploy** — commit and push the generated YAML
|
|
1289
1363
|
- **Status** — GitHub Actions UI or \`gh run list\`
|
|
1290
|
-
- **Troubleshooting** — lint rule codes (GHA001–GHA020), post-synth checks (GHA006–
|
|
1364
|
+
- **Troubleshooting** — lint rule codes (GHA001–GHA020), post-synth checks (GHA006–GHA058)
|
|
1291
1365
|
|
|
1292
1366
|
The skill is invocable as a slash command: \`/chant-github\`
|
|
1293
1367
|
|
|
@@ -1311,6 +1385,13 @@ The lexicon provides MCP (Model Context Protocol) tools and resources that AI ag
|
|
|
1311
1385
|
| MCP tool | Description |
|
|
1312
1386
|
|----------|-------------|
|
|
1313
1387
|
| \`diff\` | Compare current build output against previous output |
|
|
1388
|
+
| \`github:checks\` | Build the workflow and return its security findings (the GHA checks) |
|
|
1389
|
+
| \`github:workflow\` | Triggers and jobs as written — name, run order, step count |
|
|
1390
|
+
| \`github:references\` | Actions and images pulled in, and whether each is pinned to a commit SHA |
|
|
1391
|
+
| \`github:affected\` | Given a job, the jobs that would re-run because they depend on it |
|
|
1392
|
+
| \`github:workflow-yaml\` | The generated workflow YAML |
|
|
1393
|
+
|
|
1394
|
+
The \`github:*\` tools are **read-only context tools** (#327): each builds from your source and returns what chant already computes — before the workflow runs or merges. None touch the live GitHub instance, run history, or write anything.
|
|
1314
1395
|
|
|
1315
1396
|
| MCP resource | Description |
|
|
1316
1397
|
|--------------|-------------|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { githubContextTools, downstreamJobs, actionPinned } from "./context-tools";
|
|
6
|
+
|
|
7
|
+
const tools = Object.fromEntries(githubContextTools().map((t) => [t.name, t]));
|
|
8
|
+
|
|
9
|
+
async function call(name: string, params: Record<string, unknown>): Promise<unknown> {
|
|
10
|
+
return tools[name].handler(params);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("downstreamJobs / actionPinned (#327)", () => {
|
|
14
|
+
test("downstreamJobs follows the needs chain transitively", () => {
|
|
15
|
+
const yaml = `jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- run: echo build
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
needs: [build]
|
|
23
|
+
steps:
|
|
24
|
+
- run: echo test
|
|
25
|
+
deploy:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
needs: [test]
|
|
28
|
+
steps:
|
|
29
|
+
- run: echo deploy
|
|
30
|
+
`;
|
|
31
|
+
expect(downstreamJobs(yaml, "build").sort()).toEqual(["deploy", "test"]);
|
|
32
|
+
expect(downstreamJobs(yaml, "test")).toEqual(["deploy"]);
|
|
33
|
+
expect(downstreamJobs(yaml, "deploy")).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("actionPinned only accepts a full commit SHA", () => {
|
|
37
|
+
expect(actionPinned("actions/checkout@" + "a".repeat(40))).toBe(true);
|
|
38
|
+
expect(actionPinned("actions/checkout@v4")).toBe(false);
|
|
39
|
+
expect(actionPinned("actions/checkout@main")).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("github context tools — end to end (#327)", () => {
|
|
44
|
+
let dir: string;
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
dir = join(tmpdir(), `chant-mcp-gh-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
47
|
+
await mkdir(dir, { recursive: true });
|
|
48
|
+
await writeFile(
|
|
49
|
+
join(dir, "workflow.github.ts"),
|
|
50
|
+
`const M = Symbol.for("chant.declarable");
|
|
51
|
+
export const ci = { [M]: true, lexicon: "github", entityType: "GitHub::Actions::Workflow", kind: "resource",
|
|
52
|
+
props: {
|
|
53
|
+
name: "CI",
|
|
54
|
+
on: { push: { branches: ["main"] } },
|
|
55
|
+
permissions: { contents: "read" },
|
|
56
|
+
jobs: {
|
|
57
|
+
build: { runsOn: "ubuntu-latest", steps: [ { uses: "peter-evans/create-pull-request@v6" }, { run: "npm ci" } ] },
|
|
58
|
+
test: { runsOn: "ubuntu-latest", needs: ["build"], steps: [ { run: "npm test" } ] },
|
|
59
|
+
},
|
|
60
|
+
} };
|
|
61
|
+
`,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
await rm(dir, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("github:workflow returns triggers and jobs", async () => {
|
|
69
|
+
const out = (await call("github:workflow", { path: dir })) as {
|
|
70
|
+
workflow: string | null;
|
|
71
|
+
triggers: string[];
|
|
72
|
+
jobs: Array<{ name: string; runsAfter: string[]; steps: number }>;
|
|
73
|
+
};
|
|
74
|
+
expect(out.workflow).toBe("CI");
|
|
75
|
+
expect(out.triggers).toContain("push");
|
|
76
|
+
const names = out.jobs.map((j) => j.name);
|
|
77
|
+
expect(names).toEqual(expect.arrayContaining(["build", "test"]));
|
|
78
|
+
expect(out.jobs.find((j) => j.name === "test")?.runsAfter).toContain("build");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("github:references reports the unpinned third-party action", async () => {
|
|
82
|
+
const out = (await call("github:references", { path: dir })) as Array<{ kind: string; source: string; pinned: boolean }>;
|
|
83
|
+
const action = out.find((r) => r.kind === "action" && r.source === "peter-evans/create-pull-request@v6");
|
|
84
|
+
expect(action).toBeDefined();
|
|
85
|
+
expect(action?.pinned).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("github:checks returns the GHA findings (unpinned action trips GHA029)", async () => {
|
|
89
|
+
const out = (await call("github:checks", { path: dir })) as { findings: Array<{ id: string; severity: string; job: string | null; message: string }> };
|
|
90
|
+
expect(Array.isArray(out.findings)).toBe(true);
|
|
91
|
+
expect(out.findings.some((f) => f.id === "GHA029")).toBe(true);
|
|
92
|
+
for (const f of out.findings) {
|
|
93
|
+
expect(typeof f.id).toBe("string");
|
|
94
|
+
expect(typeof f.message).toBe("string");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("github:affected lists downstream jobs", async () => {
|
|
99
|
+
const out = (await call("github:affected", { path: dir, job: "build" })) as { job: string; wouldRerun: string[] };
|
|
100
|
+
expect(out.wouldRerun).toContain("test");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("github:workflow-yaml returns the generated YAML for a path", async () => {
|
|
104
|
+
const out = (await call("github:workflow-yaml", { path: dir })) as { yaml: string };
|
|
105
|
+
expect(typeof out.yaml).toBe("string");
|
|
106
|
+
expect(out.yaml).toContain("jobs:");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -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
|
@@ -125,8 +125,17 @@ describe("githubPlugin", () => {
|
|
|
125
125
|
|
|
126
126
|
test("provides MCP tools", () => {
|
|
127
127
|
const tools = githubPlugin.mcpTools!();
|
|
128
|
-
|
|
129
|
-
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
|
+
}
|
|
130
139
|
});
|
|
131
140
|
|
|
132
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() {
|