Skip to content

Commit 11929e5

Browse files
mr-danyacodex
andcommitted
chore: merge upstream main and resolve package version conflicts
Preserve selected event package versions alongside suspect commit output and adapt package-selection fixtures to numeric issue event endpoints. Co-Authored-By: GPT-6 (Codex) <noreply@openai.com>
2 parents 151d53d + 0563bde commit 11929e5

170 files changed

Lines changed: 18696 additions & 4693 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import assert from "node:assert/strict";
2+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+
import { createRequire } from "node:module";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
6+
import test from "node:test";
7+
8+
// Execute the actual workflow script so the tests cannot drift from production.
9+
const workflow = readFileSync(
10+
new URL("../workflows/pr-risk-jev.yml", import.meta.url),
11+
"utf8",
12+
);
13+
const script = workflow.match(/^ {10}script: \|\n((?: {12}.*\n|\n)+)/m)?.[1];
14+
assert.ok(script, "Publish risk label script must exist");
15+
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
16+
const publish = new AsyncFunction(
17+
"require",
18+
"github",
19+
"context",
20+
"core",
21+
script,
22+
);
23+
const require = createRequire(import.meta.url);
24+
const repo = { owner: "example", repo: "cli" };
25+
const head = "a".repeat(40);
26+
const base = "b".repeat(40);
27+
28+
async function withPR(options, check) {
29+
const directory = mkdtempSync(join(tmpdir(), "pr-risk-labels-"));
30+
const previousDirectory = process.cwd();
31+
const previousNumber = process.env.PR_NUMBER;
32+
const labels = new Set(options.labels ?? ["area: auth", "risk: high"]);
33+
const repositoryLabels = new Set(
34+
options.repositoryLabels ?? ["risk: low", "risk: medium", "risk: high"],
35+
);
36+
const writes = [];
37+
const warnings = [];
38+
const pr = {
39+
state: "open",
40+
head: { sha: head },
41+
base: { sha: base },
42+
title: "Update behavior",
43+
body: "Describe this change",
44+
...options.pr,
45+
};
46+
const result = {
47+
repo: "example/cli",
48+
number: 42,
49+
status: "ok",
50+
risk_label: "low",
51+
snapshot: { source_head_sha: head, source_base_sha: base },
52+
...options.result,
53+
};
54+
const error = (status) =>
55+
Object.assign(new Error(`GitHub HTTP ${status}`), { status });
56+
const github = {
57+
rest: {
58+
pulls: {
59+
get: async () => ({
60+
data: structuredClone({
61+
...pr,
62+
labels: [...labels].map((name) => ({ name })),
63+
}),
64+
}),
65+
},
66+
issues: {
67+
getLabel: async ({ name }) => {
68+
if (!repositoryLabels.has(name)) throw error(404);
69+
return { data: { name } };
70+
},
71+
createLabel: async ({ name }) => {
72+
repositoryLabels.add(name);
73+
if (options.createRace) throw error(422);
74+
return { data: { name } };
75+
},
76+
addLabels: async ({ labels: added }) => {
77+
writes.push(["add", ...added]);
78+
for (const label of added) labels.add(label);
79+
options.afterAdd?.(pr);
80+
},
81+
removeLabel: async ({ name }) => {
82+
writes.push(["remove", name]);
83+
if (!labels.delete(name)) throw error(404);
84+
},
85+
},
86+
},
87+
};
88+
try {
89+
process.chdir(directory);
90+
process.env.PR_NUMBER = "42";
91+
if (!options.missingResult)
92+
writeFileSync("risk-pr-result.json", JSON.stringify(result));
93+
await check({
94+
run: () =>
95+
publish(
96+
require,
97+
github,
98+
{ repo },
99+
{ warning: (message) => warnings.push(message), info: () => {} },
100+
),
101+
labels,
102+
writes,
103+
warnings,
104+
});
105+
} finally {
106+
process.chdir(previousDirectory);
107+
if (previousNumber === undefined) delete process.env.PR_NUMBER;
108+
else process.env.PR_NUMBER = previousNumber;
109+
rmSync(directory, { recursive: true, force: true });
110+
}
111+
}
112+
113+
for (const risk of ["low", "medium", "high"]) {
114+
test(`publishes ${risk}, replacing only risk labels`, async () => {
115+
await withPR(
116+
{
117+
result: { risk_label: risk },
118+
labels: ["area: auth", "risk: low", "risk: medium", "risk: high"],
119+
},
120+
async ({ run, labels }) => {
121+
await run();
122+
assert.deepEqual([...labels].sort(), ["area: auth", `risk: ${risk}`]);
123+
},
124+
);
125+
});
126+
}
127+
128+
test("context rejection removes stale risk labels and remains unclassified", async () => {
129+
await withPR(
130+
{ result: { status: "context_rejected", risk_label: null } },
131+
async ({ run, labels, warnings }) => {
132+
await run();
133+
assert.deepEqual([...labels], ["area: auth"]);
134+
assert.ok(warnings.some((message) => message.includes("unclassified")));
135+
},
136+
);
137+
});
138+
139+
test("moved or closed PRs are left untouched before publication", async (t) => {
140+
for (const pr of [
141+
{ head: { sha: "c".repeat(40) } },
142+
{ base: { sha: "d".repeat(40) } },
143+
{ state: "closed" },
144+
]) {
145+
await t.test(JSON.stringify(pr), async () => {
146+
await withPR({ pr }, async ({ run, labels, writes }) => {
147+
await run();
148+
assert.deepEqual(writes, []);
149+
assert.deepEqual([...labels], ["area: auth", "risk: high"]);
150+
});
151+
});
152+
}
153+
});
154+
155+
test("movement or metadata edits during publication roll back the verdict", async (t) => {
156+
for (const [name, afterAdd] of [
157+
[
158+
"head",
159+
(pr) => {
160+
pr.head.sha = "c".repeat(40);
161+
},
162+
],
163+
[
164+
"title",
165+
(pr) => {
166+
pr.title = "A different change";
167+
},
168+
],
169+
[
170+
"closed",
171+
(pr) => {
172+
pr.state = "closed";
173+
},
174+
],
175+
]) {
176+
await t.test(name, async () => {
177+
await withPR({ afterAdd }, async ({ run, labels }) => {
178+
await run();
179+
assert.deepEqual([...labels], ["area: auth"]);
180+
});
181+
});
182+
}
183+
});
184+
185+
test("results for another PR or an invalid risk cannot publish", async (t) => {
186+
for (const result of [
187+
{ repo: "example/another" },
188+
{ number: 43 },
189+
{ risk_label: "critical" },
190+
{ risk_label: "toString" },
191+
]) {
192+
await t.test(JSON.stringify(result), async () => {
193+
await withPR({ result }, async ({ run, writes }) => {
194+
await assert.rejects(run, /does not match/);
195+
assert.deepEqual(writes, []);
196+
});
197+
});
198+
}
199+
});
200+
201+
test("a missing result does not publish or remove labels", async () => {
202+
await withPR({ missingResult: true }, async ({ run, writes, warnings }) => {
203+
await run();
204+
assert.deepEqual(writes, []);
205+
assert.equal(warnings.length, 1);
206+
});
207+
});
208+
209+
test("concurrent repository label creation still publishes", async () => {
210+
await withPR(
211+
{ repositoryLabels: [], createRace: true },
212+
async ({ run, labels }) => {
213+
await run();
214+
assert.deepEqual([...labels], ["area: auth", "risk: low"]);
215+
},
216+
);
217+
});
218+
219+
for (const risk of ["low", "medium", "high"]) {
220+
test(`unchanged ${risk} leaves the PR timeline untouched`, async () => {
221+
await withPR(
222+
{ result: { risk_label: risk }, labels: ["area: auth", `risk: ${risk}`] },
223+
async ({ run, labels, writes }) => {
224+
await run();
225+
assert.deepEqual(writes, []);
226+
assert.deepEqual([...labels], ["area: auth", `risk: ${risk}`]);
227+
},
228+
);
229+
});
230+
}
231+
232+
test("an existing verdict removes competing risk labels without re-adding itself", async () => {
233+
await withPR(
234+
{
235+
result: { risk_label: "medium" },
236+
labels: ["area: auth", "risk: medium", "risk: high"],
237+
},
238+
async ({ run, labels, writes }) => {
239+
await run();
240+
assert.deepEqual(writes, [["remove", "risk: high"]]);
241+
assert.deepEqual([...labels], ["area: auth", "risk: medium"]);
242+
},
243+
);
244+
});
245+
246+
test("a second identical publication makes no label mutations", async () => {
247+
await withPR({}, async ({ run, labels, writes }) => {
248+
await run();
249+
assert.deepEqual([...labels], ["area: auth", "risk: low"]);
250+
writes.length = 0;
251+
await run();
252+
assert.deepEqual(writes, []);
253+
assert.deepEqual([...labels], ["area: auth", "risk: low"]);
254+
});
255+
});

‎.github/workflows/mcp-registry.yml‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
name: Publish MCP Registry
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: Existing stable release (e.g. 0.40.0)
10+
required: true
11+
type: string
12+
13+
permissions:
14+
contents: read
15+
16+
defaults:
17+
run:
18+
shell: bash
19+
20+
concurrency:
21+
group: mcp-registry-${{ inputs.version || github.event.release.tag_name }}
22+
cancel-in-progress: false
23+
24+
jobs:
25+
publish:
26+
if: >-
27+
github.repository == 'getsentry/sentry-mcp' &&
28+
((github.event_name == 'release' && !github.event.release.prerelease) ||
29+
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'))
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 15
32+
permissions:
33+
contents: read
34+
id-token: write
35+
env:
36+
VERSION: ${{ inputs.version || github.event.release.tag_name }}
37+
steps:
38+
# Use the maintained manifest on main, including for older releases.
39+
- uses: actions/checkout@v4
40+
with:
41+
ref: main
42+
persist-credentials: false
43+
44+
- name: Prepare metadata
45+
env:
46+
GH_TOKEN: ${{ github.token }}
47+
run: |
48+
[[ "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
49+
gh release view "$VERSION" --repo getsentry/sentry-mcp --json isDraft,isPrerelease \
50+
| jq -e '(.isDraft | not) and (.isPrerelease | not)'
51+
jq --arg version "$VERSION" '.version = $version | .packages[0].version = $version' \
52+
server.json > "$RUNNER_TEMP/server.json"
53+
54+
# Craft publishes the GitHub release before npm. Wait for the exact package.
55+
curl --fail --silent --show-error --retry 40 --retry-all-errors \
56+
--retry-delay 15 --retry-max-time 600 --max-time 15 \
57+
"https://registry.npmjs.org/@sentry%2Fmcp-server/$VERSION" \
58+
-o "$RUNNER_TEMP/npm.json"
59+
jq -e --arg version "$VERSION" --slurpfile manifest "$RUNNER_TEMP/server.json" \
60+
'.name == "@sentry/mcp-server" and .version == $version and .mcpName == $manifest[0].name' \
61+
"$RUNNER_TEMP/npm.json"
62+
63+
- name: Check for an existing listing
64+
id: existing
65+
run: |
66+
url="https://registry.modelcontextprotocol.io/v0.1/servers/io.github.getsentry%2Fsentry-mcp/versions/$VERSION"
67+
echo "url=$url" >> "$GITHUB_OUTPUT"
68+
status=$(curl --silent --show-error --max-time 30 -o "$RUNNER_TEMP/existing.json" -w '%{http_code}' "$url")
69+
case "$status" in
70+
404) echo 'publish=true' >> "$GITHUB_OUTPUT" ;;
71+
200) ;; # Verify the existing record below instead of republishing it.
72+
*) echo "Registry lookup failed: HTTP $status" >&2; exit 1 ;;
73+
esac
74+
75+
- name: Install MCP publisher
76+
if: steps.existing.outputs.publish == 'true'
77+
working-directory: ${{ runner.temp }}
78+
env:
79+
# Update both from https://github.com/modelcontextprotocol/registry/releases
80+
# Use the mcp-publisher_linux_amd64.tar.gz entry in registry_<version>_checksums.txt.
81+
MCP_PUBLISHER_VERSION: "1.8.1"
82+
MCP_PUBLISHER_SHA256: "a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc"
83+
run: |
84+
curl --fail --silent --show-error --location --retry 3 \
85+
"https://github.com/modelcontextprotocol/registry/releases/download/v${MCP_PUBLISHER_VERSION}/mcp-publisher_linux_amd64.tar.gz" \
86+
-o mcp-publisher.tar.gz
87+
echo "$MCP_PUBLISHER_SHA256 mcp-publisher.tar.gz" | sha256sum --check
88+
tar xzf mcp-publisher.tar.gz mcp-publisher
89+
90+
- name: Publish metadata
91+
if: steps.existing.outputs.publish == 'true'
92+
working-directory: ${{ runner.temp }}
93+
run: |
94+
./mcp-publisher login github-oidc
95+
./mcp-publisher publish
96+
97+
- name: Verify listing
98+
env:
99+
REGISTRY_URL: ${{ steps.existing.outputs.url }}
100+
run: |
101+
curl --fail --silent --show-error --retry 5 --retry-all-errors --retry-delay 10 --max-time 15 \
102+
"$REGISTRY_URL" -o "$RUNNER_TEMP/published.json"
103+
jq -e --slurpfile expected "$RUNNER_TEMP/server.json" \
104+
'(.server | del(.["$schema"])) == ($expected[0] | del(.["$schema"])) and
105+
._meta["io.modelcontextprotocol.registry/official"].status == "active"' \
106+
"$RUNNER_TEMP/published.json"

0 commit comments

Comments
 (0)