mcp-server-diff 2.1.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/.github/dependabot.yml +21 -0
- package/.github/workflows/ci.yml +51 -0
- package/.github/workflows/publish.yml +36 -0
- package/.github/workflows/release.yml +51 -0
- package/.prettierignore +3 -0
- package/.prettierrc +8 -0
- package/CONTRIBUTING.md +81 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/action.yml +250 -0
- package/dist/__tests__/fixtures/http-server.d.ts +7 -0
- package/dist/__tests__/fixtures/stdio-server.d.ts +7 -0
- package/dist/cli/__tests__/fixtures/http-server.d.ts +7 -0
- package/dist/cli/__tests__/fixtures/stdio-server.d.ts +7 -0
- package/dist/cli/cli.d.ts +7 -0
- package/dist/cli/diff.d.ts +44 -0
- package/dist/cli/git.d.ts +37 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +57182 -0
- package/dist/cli/licenses.txt +466 -0
- package/dist/cli/logger.d.ts +46 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/probe.d.ts +35 -0
- package/dist/cli/reporter.d.ts +20 -0
- package/dist/cli/runner.d.ts +30 -0
- package/dist/cli/types.d.ts +134 -0
- package/dist/cli.d.ts +7 -0
- package/dist/diff.d.ts +44 -0
- package/dist/git.d.ts +37 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +58032 -0
- package/dist/licenses.txt +466 -0
- package/dist/logger.d.ts +46 -0
- package/dist/package.json +3 -0
- package/dist/probe.d.ts +35 -0
- package/dist/reporter.d.ts +20 -0
- package/dist/runner.d.ts +30 -0
- package/dist/types.d.ts +134 -0
- package/eslint.config.mjs +47 -0
- package/jest.config.mjs +26 -0
- package/package.json +64 -0
- package/src/__tests__/fixtures/http-server.ts +103 -0
- package/src/__tests__/fixtures/stdio-server.ts +158 -0
- package/src/__tests__/integration.test.ts +306 -0
- package/src/__tests__/runner.test.ts +430 -0
- package/src/cli.ts +421 -0
- package/src/diff.ts +252 -0
- package/src/git.ts +262 -0
- package/src/index.ts +284 -0
- package/src/logger.ts +93 -0
- package/src/probe.ts +327 -0
- package/src/reporter.ts +214 -0
- package/src/runner.ts +902 -0
- package/src/types.ts +155 -0
- package/tsconfig.json +30 -0
package/src/git.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utilities for MCP server diff
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as exec from "@actions/exec";
|
|
6
|
+
import * as core from "@actions/core";
|
|
7
|
+
|
|
8
|
+
export interface GitInfo {
|
|
9
|
+
currentBranch: string;
|
|
10
|
+
compareRef: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get current branch name
|
|
15
|
+
*/
|
|
16
|
+
export async function getCurrentBranch(): Promise<string> {
|
|
17
|
+
let output = "";
|
|
18
|
+
try {
|
|
19
|
+
await exec.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
20
|
+
silent: true,
|
|
21
|
+
listeners: {
|
|
22
|
+
stdout: (data) => {
|
|
23
|
+
output += data.toString();
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return output.trim() || "HEAD";
|
|
28
|
+
} catch {
|
|
29
|
+
return "HEAD";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Determine what ref to compare against
|
|
35
|
+
* Priority: 1) Explicit compare_ref, 2) Auto-detect previous tag, 3) Merge-base with main
|
|
36
|
+
*/
|
|
37
|
+
export async function determineCompareRef(
|
|
38
|
+
explicitRef?: string,
|
|
39
|
+
githubRef?: string
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
// If explicit ref provided, use it
|
|
42
|
+
if (explicitRef) {
|
|
43
|
+
core.info(`Using explicit compare ref: ${explicitRef}`);
|
|
44
|
+
return explicitRef;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if this is a tag push
|
|
48
|
+
if (githubRef?.startsWith("refs/tags/")) {
|
|
49
|
+
const currentTag = githubRef.replace("refs/tags/", "");
|
|
50
|
+
core.info(`Detected tag push: ${currentTag}`);
|
|
51
|
+
|
|
52
|
+
// Try to find previous tag
|
|
53
|
+
const previousTag = await findPreviousTag(currentTag);
|
|
54
|
+
if (previousTag && previousTag !== currentTag) {
|
|
55
|
+
core.info(`Auto-detected previous tag: ${previousTag}`);
|
|
56
|
+
return previousTag;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fall back to first commit
|
|
60
|
+
const firstCommit = await getFirstCommit();
|
|
61
|
+
core.warning("No previous tag found, comparing against initial commit");
|
|
62
|
+
return firstCommit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Default: find merge-base with main
|
|
66
|
+
const baseRef = await findMainBranch();
|
|
67
|
+
const mergeBase = await getMergeBase(baseRef);
|
|
68
|
+
core.info(`Using merge-base with ${baseRef}: ${mergeBase}`);
|
|
69
|
+
return mergeBase;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find the previous tag (sorted by version)
|
|
74
|
+
*/
|
|
75
|
+
async function findPreviousTag(currentTag: string): Promise<string | null> {
|
|
76
|
+
let output = "";
|
|
77
|
+
try {
|
|
78
|
+
await exec.exec("git", ["tag", "--sort=-v:refname"], {
|
|
79
|
+
silent: true,
|
|
80
|
+
listeners: {
|
|
81
|
+
stdout: (data) => {
|
|
82
|
+
output += data.toString();
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const tags = output.trim().split("\n");
|
|
88
|
+
const currentIndex = tags.indexOf(currentTag);
|
|
89
|
+
if (currentIndex >= 0 && currentIndex < tags.length - 1) {
|
|
90
|
+
return tags[currentIndex + 1];
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the first commit in the repository
|
|
100
|
+
*/
|
|
101
|
+
async function getFirstCommit(): Promise<string> {
|
|
102
|
+
let output = "";
|
|
103
|
+
await exec.exec("git", ["rev-list", "--max-parents=0", "HEAD"], {
|
|
104
|
+
silent: true,
|
|
105
|
+
listeners: {
|
|
106
|
+
stdout: (data) => {
|
|
107
|
+
output += data.toString();
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return output.trim().split("\n")[0];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Find the main branch (origin/main, main, or first commit)
|
|
116
|
+
*/
|
|
117
|
+
async function findMainBranch(): Promise<string> {
|
|
118
|
+
// Try origin/main
|
|
119
|
+
try {
|
|
120
|
+
await exec.exec("git", ["rev-parse", "--verify", "origin/main"], { silent: true });
|
|
121
|
+
return "origin/main";
|
|
122
|
+
} catch {
|
|
123
|
+
// Try main
|
|
124
|
+
try {
|
|
125
|
+
await exec.exec("git", ["rev-parse", "--verify", "main"], { silent: true });
|
|
126
|
+
return "main";
|
|
127
|
+
} catch {
|
|
128
|
+
// Fall back to first commit
|
|
129
|
+
return await getFirstCommit();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get merge-base between HEAD and a ref
|
|
136
|
+
*/
|
|
137
|
+
async function getMergeBase(ref: string): Promise<string> {
|
|
138
|
+
let output = "";
|
|
139
|
+
try {
|
|
140
|
+
await exec.exec("git", ["merge-base", "HEAD", ref], {
|
|
141
|
+
silent: true,
|
|
142
|
+
listeners: {
|
|
143
|
+
stdout: (data) => {
|
|
144
|
+
output += data.toString();
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return output.trim();
|
|
149
|
+
} catch {
|
|
150
|
+
return ref;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a worktree for the compare ref
|
|
156
|
+
*/
|
|
157
|
+
export async function createWorktree(ref: string, path: string): Promise<boolean> {
|
|
158
|
+
try {
|
|
159
|
+
await exec.exec("git", ["worktree", "add", "--quiet", path, ref], { silent: true });
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Remove a worktree
|
|
168
|
+
*/
|
|
169
|
+
export async function removeWorktree(path: string): Promise<void> {
|
|
170
|
+
try {
|
|
171
|
+
await exec.exec("git", ["worktree", "remove", "--force", path], { silent: true });
|
|
172
|
+
} catch {
|
|
173
|
+
// Ignore errors
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Checkout a ref (fallback if worktree fails)
|
|
179
|
+
*/
|
|
180
|
+
export async function checkout(ref: string): Promise<void> {
|
|
181
|
+
await exec.exec("git", ["checkout", "--quiet", ref], { silent: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Checkout previous branch/ref
|
|
186
|
+
*/
|
|
187
|
+
export async function checkoutPrevious(): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
await exec.exec("git", ["checkout", "--quiet", "-"], { silent: true });
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore errors
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get a display-friendly name for a ref.
|
|
197
|
+
* Returns branch/tag name if available, otherwise the short SHA.
|
|
198
|
+
*/
|
|
199
|
+
export async function getRefDisplayName(ref: string): Promise<string> {
|
|
200
|
+
// If it's already a readable name (not a SHA), return it
|
|
201
|
+
if (!ref.match(/^[a-f0-9]{40}$/i) && !ref.match(/^[a-f0-9]{7,}$/i)) {
|
|
202
|
+
// It's likely already a branch/tag name
|
|
203
|
+
return ref;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Try to find a branch name pointing to this ref
|
|
207
|
+
let output = "";
|
|
208
|
+
try {
|
|
209
|
+
await exec.exec("git", ["branch", "--points-at", ref, "--format=%(refname:short)"], {
|
|
210
|
+
silent: true,
|
|
211
|
+
listeners: {
|
|
212
|
+
stdout: (data) => {
|
|
213
|
+
output += data.toString();
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const branches = output.trim().split("\n").filter(Boolean);
|
|
218
|
+
if (branches.length > 0) {
|
|
219
|
+
// Prefer main/master if available
|
|
220
|
+
if (branches.includes("main")) return "main";
|
|
221
|
+
if (branches.includes("master")) return "master";
|
|
222
|
+
return branches[0];
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Ignore errors
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Try to find a tag pointing to this ref
|
|
229
|
+
output = "";
|
|
230
|
+
try {
|
|
231
|
+
await exec.exec("git", ["tag", "--points-at", ref], {
|
|
232
|
+
silent: true,
|
|
233
|
+
listeners: {
|
|
234
|
+
stdout: (data) => {
|
|
235
|
+
output += data.toString();
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
const tags = output.trim().split("\n").filter(Boolean);
|
|
240
|
+
if (tags.length > 0) {
|
|
241
|
+
return tags[0];
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// Ignore errors
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Fall back to short SHA
|
|
248
|
+
output = "";
|
|
249
|
+
try {
|
|
250
|
+
await exec.exec("git", ["rev-parse", "--short", ref], {
|
|
251
|
+
silent: true,
|
|
252
|
+
listeners: {
|
|
253
|
+
stdout: (data) => {
|
|
254
|
+
output += data.toString();
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
return output.trim() || ref;
|
|
259
|
+
} catch {
|
|
260
|
+
return ref.substring(0, 7);
|
|
261
|
+
}
|
|
262
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Diff - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Diffs MCP server public interfaces by comparing
|
|
5
|
+
* API responses between the current branch and a reference.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as core from "@actions/core";
|
|
9
|
+
import * as exec from "@actions/exec";
|
|
10
|
+
import { getCurrentBranch, determineCompareRef, getRefDisplayName } from "./git.js";
|
|
11
|
+
import { parseConfigurations, parseCustomMessages, parseHeaders, runAllTests } from "./runner.js";
|
|
12
|
+
import { generateReport, generateMarkdownReport, saveReport } from "./reporter.js";
|
|
13
|
+
import type { ActionInputs } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get all inputs from the action (composite action style - INPUT_* env vars)
|
|
17
|
+
*/
|
|
18
|
+
function getInputs(): ActionInputs {
|
|
19
|
+
// Helper to get input from INPUT_* environment variables
|
|
20
|
+
const getInput = (name: string): string => {
|
|
21
|
+
const envName = `INPUT_${name.toUpperCase().replace(/-/g, "_")}`;
|
|
22
|
+
return process.env[envName] || "";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getBooleanInput = (name: string): boolean => {
|
|
26
|
+
const value = getInput(name);
|
|
27
|
+
return value.toLowerCase() === "true";
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const transport = (getInput("transport") || "stdio") as "stdio" | "streamable-http";
|
|
31
|
+
const startCommand = getInput("start_command");
|
|
32
|
+
const serverUrl = getInput("server_url");
|
|
33
|
+
|
|
34
|
+
// Parse configurations
|
|
35
|
+
const configurationsInput = getInput("configurations");
|
|
36
|
+
const configurations = parseConfigurations(
|
|
37
|
+
configurationsInput,
|
|
38
|
+
transport,
|
|
39
|
+
startCommand,
|
|
40
|
+
serverUrl
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Parse custom messages
|
|
44
|
+
const customMessagesInput = getInput("custom_messages");
|
|
45
|
+
const customMessages = parseCustomMessages(customMessagesInput);
|
|
46
|
+
|
|
47
|
+
// Parse global headers
|
|
48
|
+
const headersInput = getInput("headers");
|
|
49
|
+
const headers = parseHeaders(headersInput);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
// Language setup
|
|
53
|
+
setupNode: getBooleanInput("setup_node"),
|
|
54
|
+
nodeVersion: getInput("node_version") || "20",
|
|
55
|
+
setupPython: getBooleanInput("setup_python"),
|
|
56
|
+
pythonVersion: getInput("python_version") || "3.11",
|
|
57
|
+
setupGo: getBooleanInput("setup_go"),
|
|
58
|
+
goVersion: getInput("go_version") || "1.24",
|
|
59
|
+
setupRust: getBooleanInput("setup_rust"),
|
|
60
|
+
rustToolchain: getInput("rust_toolchain") || "stable",
|
|
61
|
+
setupDotnet: getBooleanInput("setup_dotnet"),
|
|
62
|
+
dotnetVersion: getInput("dotnet_version") || "9.0.x",
|
|
63
|
+
|
|
64
|
+
// Build configuration
|
|
65
|
+
installCommand: getInput("install_command"),
|
|
66
|
+
buildCommand: getInput("build_command"),
|
|
67
|
+
startCommand,
|
|
68
|
+
|
|
69
|
+
// Transport configuration
|
|
70
|
+
transport,
|
|
71
|
+
serverUrl,
|
|
72
|
+
headers,
|
|
73
|
+
configurations,
|
|
74
|
+
customMessages,
|
|
75
|
+
|
|
76
|
+
// Shared HTTP server configuration
|
|
77
|
+
httpStartCommand: getInput("http_start_command"),
|
|
78
|
+
httpStartupWaitMs: parseInt(getInput("http_startup_wait_ms") || "2000", 10),
|
|
79
|
+
|
|
80
|
+
// Test configuration
|
|
81
|
+
compareRef: getInput("compare_ref"),
|
|
82
|
+
failOnError: getBooleanInput("fail_on_error") !== false, // default true
|
|
83
|
+
failOnDiff: getBooleanInput("fail_on_diff") === true, // default false
|
|
84
|
+
envVars: getInput("env_vars"),
|
|
85
|
+
serverTimeout: parseInt(getInput("server_timeout") || "30000", 10),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set up language runtimes based on inputs
|
|
91
|
+
*/
|
|
92
|
+
async function setupLanguages(inputs: ActionInputs): Promise<void> {
|
|
93
|
+
// We rely on composite action setup or assume runtimes are available
|
|
94
|
+
// In a pure Node action, we'd need to install these ourselves or
|
|
95
|
+
// require the user to set them up in a prior step
|
|
96
|
+
|
|
97
|
+
core.info("๐ฆ Verifying language runtimes...");
|
|
98
|
+
|
|
99
|
+
if (inputs.setupNode) {
|
|
100
|
+
try {
|
|
101
|
+
let output = "";
|
|
102
|
+
await exec.exec("node", ["--version"], {
|
|
103
|
+
silent: true,
|
|
104
|
+
listeners: {
|
|
105
|
+
stdout: (data) => {
|
|
106
|
+
output += data.toString();
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
core.info(` Node.js: ${output.trim()}`);
|
|
111
|
+
} catch {
|
|
112
|
+
core.warning("Node.js not available - please set up in a prior step");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (inputs.setupPython) {
|
|
117
|
+
try {
|
|
118
|
+
let output = "";
|
|
119
|
+
await exec.exec("python", ["--version"], {
|
|
120
|
+
silent: true,
|
|
121
|
+
listeners: {
|
|
122
|
+
stdout: (data) => {
|
|
123
|
+
output += data.toString();
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
core.info(` Python: ${output.trim()}`);
|
|
128
|
+
} catch {
|
|
129
|
+
core.warning("Python not available - please set up in a prior step");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (inputs.setupGo) {
|
|
134
|
+
try {
|
|
135
|
+
let output = "";
|
|
136
|
+
await exec.exec("go", ["version"], {
|
|
137
|
+
silent: true,
|
|
138
|
+
listeners: {
|
|
139
|
+
stdout: (data) => {
|
|
140
|
+
output += data.toString();
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
core.info(` Go: ${output.trim()}`);
|
|
145
|
+
} catch {
|
|
146
|
+
core.warning("Go not available - please set up in a prior step");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (inputs.setupRust) {
|
|
151
|
+
try {
|
|
152
|
+
let output = "";
|
|
153
|
+
await exec.exec("rustc", ["--version"], {
|
|
154
|
+
silent: true,
|
|
155
|
+
listeners: {
|
|
156
|
+
stdout: (data) => {
|
|
157
|
+
output += data.toString();
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
core.info(` Rust: ${output.trim()}`);
|
|
162
|
+
} catch {
|
|
163
|
+
core.warning("Rust not available - please set up in a prior step");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (inputs.setupDotnet) {
|
|
168
|
+
try {
|
|
169
|
+
let output = "";
|
|
170
|
+
await exec.exec("dotnet", ["--version"], {
|
|
171
|
+
silent: true,
|
|
172
|
+
listeners: {
|
|
173
|
+
stdout: (data) => {
|
|
174
|
+
output += data.toString();
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
core.info(` .NET: ${output.trim()}`);
|
|
179
|
+
} catch {
|
|
180
|
+
core.warning(".NET not available - please set up in a prior step");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Run initial build for current branch
|
|
187
|
+
*/
|
|
188
|
+
async function runInitialBuild(inputs: ActionInputs): Promise<void> {
|
|
189
|
+
core.info("๐จ Running initial build...");
|
|
190
|
+
|
|
191
|
+
if (inputs.installCommand) {
|
|
192
|
+
core.info(` Install: ${inputs.installCommand}`);
|
|
193
|
+
await exec.exec("sh", ["-c", inputs.installCommand]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (inputs.buildCommand) {
|
|
197
|
+
core.info(` Build: ${inputs.buildCommand}`);
|
|
198
|
+
await exec.exec("sh", ["-c", inputs.buildCommand]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Main action entry point
|
|
204
|
+
*/
|
|
205
|
+
async function run(): Promise<void> {
|
|
206
|
+
try {
|
|
207
|
+
core.info("๐ MCP Conformance Action");
|
|
208
|
+
core.info("");
|
|
209
|
+
|
|
210
|
+
// Get inputs
|
|
211
|
+
const inputs = getInputs();
|
|
212
|
+
|
|
213
|
+
core.info(`๐ Configuration:`);
|
|
214
|
+
core.info(` Transport: ${inputs.transport}`);
|
|
215
|
+
core.info(` Configurations: ${inputs.configurations.length}`);
|
|
216
|
+
for (const config of inputs.configurations) {
|
|
217
|
+
core.info(` - ${config.name} (${config.transport})`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Set up languages
|
|
221
|
+
await setupLanguages(inputs);
|
|
222
|
+
|
|
223
|
+
// Run initial build
|
|
224
|
+
await runInitialBuild(inputs);
|
|
225
|
+
|
|
226
|
+
// Determine comparison ref
|
|
227
|
+
const currentBranch = await getCurrentBranch();
|
|
228
|
+
const compareRef = await determineCompareRef(inputs.compareRef, process.env.GITHUB_REF);
|
|
229
|
+
const compareRefDisplay = await getRefDisplayName(compareRef);
|
|
230
|
+
|
|
231
|
+
core.info("");
|
|
232
|
+
core.info(`๐ Comparison:`);
|
|
233
|
+
core.info(` Current: ${currentBranch}`);
|
|
234
|
+
core.info(
|
|
235
|
+
` Compare: ${compareRefDisplay}${compareRefDisplay !== compareRef ? ` (${compareRef.substring(0, 7)})` : ""}`
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Run all tests
|
|
239
|
+
core.info("");
|
|
240
|
+
core.info("๐งช Running diff...");
|
|
241
|
+
|
|
242
|
+
const workDir = process.cwd();
|
|
243
|
+
const results = await runAllTests({
|
|
244
|
+
workDir,
|
|
245
|
+
inputs,
|
|
246
|
+
compareRef,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Generate and save report
|
|
250
|
+
core.info("");
|
|
251
|
+
core.info("๐ Generating report...");
|
|
252
|
+
|
|
253
|
+
const report = generateReport(results, currentBranch, compareRefDisplay);
|
|
254
|
+
const markdown = generateMarkdownReport(report);
|
|
255
|
+
saveReport(report, markdown, workDir);
|
|
256
|
+
|
|
257
|
+
// Set final status
|
|
258
|
+
core.info("");
|
|
259
|
+
|
|
260
|
+
// Check for actual probe errors (separate from differences)
|
|
261
|
+
const hasErrors = results.some((r) => r.diffs.has("error"));
|
|
262
|
+
|
|
263
|
+
if (hasErrors && inputs.failOnError) {
|
|
264
|
+
const errorConfigs = results.filter((r) => r.diffs.has("error")).map((r) => r.configName);
|
|
265
|
+
core.setFailed(`โ Probe errors occurred in: ${errorConfigs.join(", ")}`);
|
|
266
|
+
} else if (report.diffCount > 0) {
|
|
267
|
+
if (inputs.failOnDiff) {
|
|
268
|
+
core.setFailed(`โ ${report.diffCount} configuration(s) have API changes`);
|
|
269
|
+
} else {
|
|
270
|
+
core.info(`๐ ${report.diffCount} configuration(s) have API changes`);
|
|
271
|
+
}
|
|
272
|
+
if (hasErrors) {
|
|
273
|
+
core.warning("Some configurations had probe errors (fail_on_error is disabled)");
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
core.info("โ
All tests passed - no API changes detected");
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
core.setFailed(`Action failed: ${error}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Run the action
|
|
284
|
+
run();
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger abstraction - works in both CLI and GitHub Actions contexts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as core from "@actions/core";
|
|
6
|
+
|
|
7
|
+
export interface Logger {
|
|
8
|
+
info(message: string): void;
|
|
9
|
+
warning(message: string): void;
|
|
10
|
+
error(message: string): void;
|
|
11
|
+
debug(message: string): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GitHub Actions logger - wraps @actions/core
|
|
16
|
+
*/
|
|
17
|
+
export class ActionsLogger implements Logger {
|
|
18
|
+
info(message: string): void {
|
|
19
|
+
core.info(message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
warning(message: string): void {
|
|
23
|
+
core.warning(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
error(message: string): void {
|
|
27
|
+
core.error(message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
debug(message: string): void {
|
|
31
|
+
core.debug(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Console logger for CLI usage
|
|
37
|
+
*/
|
|
38
|
+
export class ConsoleLogger implements Logger {
|
|
39
|
+
private verbose: boolean;
|
|
40
|
+
|
|
41
|
+
constructor(verbose: boolean = false) {
|
|
42
|
+
this.verbose = verbose;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
info(message: string): void {
|
|
46
|
+
console.log(message);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
warning(message: string): void {
|
|
50
|
+
console.log(`โ ๏ธ ${message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
error(message: string): void {
|
|
54
|
+
console.error(`โ ${message}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
debug(message: string): void {
|
|
58
|
+
if (this.verbose) {
|
|
59
|
+
console.log(`๐ ${message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Quiet logger - only outputs errors
|
|
66
|
+
*/
|
|
67
|
+
export class QuietLogger implements Logger {
|
|
68
|
+
info(_message: string): void {}
|
|
69
|
+
warning(_message: string): void {}
|
|
70
|
+
error(message: string): void {
|
|
71
|
+
console.error(message);
|
|
72
|
+
}
|
|
73
|
+
debug(_message: string): void {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Global logger instance - defaults to Actions logger for backward compat
|
|
77
|
+
let currentLogger: Logger = new ActionsLogger();
|
|
78
|
+
|
|
79
|
+
export function setLogger(logger: Logger): void {
|
|
80
|
+
currentLogger = logger;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getLogger(): Logger {
|
|
84
|
+
return currentLogger;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Convenience exports that use the current logger
|
|
88
|
+
export const log = {
|
|
89
|
+
info: (message: string) => currentLogger.info(message),
|
|
90
|
+
warning: (message: string) => currentLogger.warning(message),
|
|
91
|
+
error: (message: string) => currentLogger.error(message),
|
|
92
|
+
debug: (message: string) => currentLogger.debug(message),
|
|
93
|
+
};
|