Skip to content

Commit 911301e

Browse files
authored
ci(docs): publish manifests to website on release (#372)
* ci(docs): publish manifests to website on release Replaces the website-side weekly cron PR (which has been failing because GitHub Actions in the getsentry org cannot create PRs with the default GITHUB_TOKEN) with a release-time push, mirroring the existing publish-schemas.yml flow. - scripts/build-website-manifest.mjs reads manifests/{tools,workflows}/*.yaml and package.json, normalises to the same shape the website expects, and writes JSON to --out=. Output is byte-identical (per entry) to what the website's sync-xcodebuildmcp-manifests.mjs produces. - .github/workflows/publish-manifests.yml runs on release: published (and workflow_dispatch with an optional --ref override), checks out at the release tag, generates the JSON straight into a clone of the website repo, and commits to main over SSH using the existing XCODEBUILDMCP_WEBSITE_DEPLOY_KEY. The website-side .github/workflows/sync-xcodebuildmcp-docs.yml will be removed in a follow-up PR on getsentry/xcodebuildmcp.com. * fix(ci): widen manifest extension filter to .yaml and .yml Mirror src/core/manifest/load-manifest.ts so the website snapshot can't drift from the runtime tool list if a .yml manifest is ever added. Addresses Cursor Bugbot review on #372.
1 parent 77c206a commit 911301e

3 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Publish Manifests
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
release:
8+
types: [published]
9+
workflow_dispatch:
10+
inputs:
11+
ref:
12+
description: "Override ref/tag to publish (defaults to release tag)"
13+
required: false
14+
type: string
15+
16+
jobs:
17+
publish:
18+
runs-on: ubuntu-latest
19+
env:
20+
WEBSITE_REPO: getsentry/xcodebuildmcp.com
21+
WEBSITE_BRANCH: main
22+
TARGET_FILE: app/docs/_data/generated/manifests.json
23+
steps:
24+
- name: Checkout source repository
25+
uses: actions/checkout@v4
26+
with:
27+
ref: ${{ inputs.ref || github.event.release.tag_name || github.ref }}
28+
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: '24'
33+
34+
- name: Install dependencies
35+
run: npm install --ignore-scripts
36+
37+
- name: Fail if deploy key is missing
38+
env:
39+
DEPLOY_KEY: ${{ secrets.XCODEBUILDMCP_WEBSITE_DEPLOY_KEY }}
40+
run: |
41+
set -euo pipefail
42+
if [ -z "$DEPLOY_KEY" ]; then
43+
echo "XCODEBUILDMCP_WEBSITE_DEPLOY_KEY is required to publish manifests." >&2
44+
exit 1
45+
fi
46+
47+
- name: Configure SSH for website repository
48+
uses: webfactory/ssh-agent@v0.9.0
49+
with:
50+
ssh-private-key: ${{ secrets.XCODEBUILDMCP_WEBSITE_DEPLOY_KEY }}
51+
52+
- name: Clone website repository
53+
run: |
54+
set -euo pipefail
55+
git clone "git@github.com:${WEBSITE_REPO}.git" website-repo
56+
cd website-repo
57+
git checkout "$WEBSITE_BRANCH"
58+
git pull --ff-only origin "$WEBSITE_BRANCH"
59+
60+
- name: Resolve ref
61+
id: ref
62+
env:
63+
INPUT_REF: ${{ inputs.ref }}
64+
RELEASE_TAG: ${{ github.event.release.tag_name }}
65+
run: |
66+
set -euo pipefail
67+
REF="${INPUT_REF:-$RELEASE_TAG}"
68+
if [ -z "$REF" ]; then
69+
echo "No ref available (workflow_dispatch without input on a non-release event)." >&2
70+
exit 1
71+
fi
72+
echo "value=$REF" >> "$GITHUB_OUTPUT"
73+
74+
- name: Generate manifests.json into website repo
75+
env:
76+
REF: ${{ steps.ref.outputs.value }}
77+
run: |
78+
set -euo pipefail
79+
node scripts/build-website-manifest.mjs \
80+
--out="website-repo/${TARGET_FILE}" \
81+
--ref="$REF"
82+
83+
- name: Commit and push website update
84+
env:
85+
REF: ${{ steps.ref.outputs.value }}
86+
run: |
87+
set -euo pipefail
88+
cd website-repo
89+
git config user.name "github-actions[bot]"
90+
git config user.email "github-actions[bot]@users.noreply.github.com"
91+
git add "$TARGET_FILE"
92+
if git diff --cached --quiet; then
93+
echo "Manifest publish target already up to date."
94+
exit 0
95+
fi
96+
git commit -m "Publish manifests from ${GITHUB_REPOSITORY}@${REF}"
97+
git push origin "$WEBSITE_BRANCH"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"repro:mcp-lifecycle-leak": "npm run build && npx tsx scripts/repro-mcp-lifecycle-leak.ts",
2626
"repro:sentry-mcp-teardown": "cd repros/sentry-mcp-teardown && npm run harness",
2727
"bundle:axe": "scripts/bundle-axe.sh",
28+
"build:website-manifest": "node scripts/build-website-manifest.mjs",
2829
"package:macos": "scripts/package-macos-portable.sh",
2930
"package:macos:universal": "scripts/package-macos-portable.sh --universal",
3031
"verify:portable": "scripts/verify-portable-install.sh",

scripts/build-website-manifest.mjs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Build the manifests.json snapshot consumed by getsentry/xcodebuildmcp.com.
4+
*
5+
* Reads manifests/workflows/*.yaml, manifests/tools/*.yaml, and package.json
6+
* from this repo, normalises them into the shape the website expects, and
7+
* writes JSON to the path passed via --out=.
8+
*
9+
* Usage:
10+
* node scripts/build-website-manifest.mjs --out=<path> [--ref=<tag>]
11+
*
12+
* The output shape mirrors scripts/sync-xcodebuildmcp-manifests.mjs in the
13+
* website repo so the publish path can be flipped from pull (Monday cron PR)
14+
* to push (release-time direct commit) without changing consumers.
15+
*/
16+
17+
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
18+
import path from "node:path";
19+
import { fileURLToPath } from "node:url";
20+
import { parse as parseYaml } from "yaml";
21+
22+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
23+
const repoRoot = path.resolve(__dirname, "..");
24+
25+
function parseArgs(argv) {
26+
const args = { out: undefined, ref: undefined };
27+
for (const a of argv) {
28+
if (a.startsWith("--out=")) args.out = a.slice("--out=".length);
29+
else if (a.startsWith("--ref=")) args.ref = a.slice("--ref=".length);
30+
else if (a === "--help" || a === "-h") args.help = true;
31+
else {
32+
console.error(`Unknown argument: ${a}`);
33+
process.exit(2);
34+
}
35+
}
36+
return args;
37+
}
38+
39+
function usage() {
40+
console.error(
41+
"Usage: build-website-manifest.mjs --out=<path> [--ref=<tag>]\n" +
42+
" --out Output JSON file path (required).\n" +
43+
" --ref Ref/tag to record in the snapshot. Defaults to v<package.json version>.",
44+
);
45+
}
46+
47+
async function loadYamlDir(dir) {
48+
const entries = (await readdir(dir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
49+
return Promise.all(
50+
entries.map(async (f) => parseYaml(await readFile(path.join(dir, f), "utf8"))),
51+
);
52+
}
53+
54+
function normalizeTools(raw) {
55+
return raw
56+
.map((t) => ({
57+
id: t.id,
58+
mcpName: t.names?.mcp ?? t.id,
59+
cliName: t.names?.cli ?? null,
60+
description: t.description ?? "",
61+
title: t.annotations?.title ?? null,
62+
readOnly: Boolean(t.annotations?.readOnlyHint),
63+
destructive: Boolean(t.annotations?.destructiveHint),
64+
openWorld: Boolean(t.annotations?.openWorldHint),
65+
module: t.module ?? null,
66+
predicates: Array.isArray(t.predicates) ? t.predicates : [],
67+
}))
68+
.sort((a, b) => a.mcpName.localeCompare(b.mcpName));
69+
}
70+
71+
function normalizeWorkflows(raw) {
72+
return raw
73+
.map((w) => ({
74+
id: w.id,
75+
title: w.title ?? w.id,
76+
description: w.description ?? "",
77+
defaultEnabled: Boolean(w.selection?.mcp?.defaultEnabled),
78+
tools: Array.isArray(w.tools) ? w.tools : [],
79+
}))
80+
.sort((a, b) => a.id.localeCompare(b.id));
81+
}
82+
83+
async function main() {
84+
const args = parseArgs(process.argv.slice(2));
85+
if (args.help) {
86+
usage();
87+
return;
88+
}
89+
if (!args.out) {
90+
usage();
91+
process.exit(2);
92+
}
93+
94+
const [workflows, tools, pkg] = await Promise.all([
95+
loadYamlDir(path.join(repoRoot, "manifests", "workflows")),
96+
loadYamlDir(path.join(repoRoot, "manifests", "tools")),
97+
readFile(path.join(repoRoot, "package.json"), "utf8").then(JSON.parse),
98+
]);
99+
100+
const ref = args.ref ?? `v${pkg.version}`;
101+
const snapshot = {
102+
source: `github:getsentry/XcodeBuildMCP@${ref}`,
103+
ref,
104+
syncedAt: new Date().toISOString(),
105+
version: pkg.version,
106+
workflows: normalizeWorkflows(workflows),
107+
tools: normalizeTools(tools),
108+
};
109+
110+
const outPath = path.resolve(args.out);
111+
await mkdir(path.dirname(outPath), { recursive: true });
112+
await writeFile(outPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
113+
114+
console.log(
115+
`Wrote ${outPath}\n ref: ${ref}\n version: ${snapshot.version}\n workflows: ${snapshot.workflows.length}\n tools: ${snapshot.tools.length}`,
116+
);
117+
}
118+
119+
main().catch((err) => {
120+
console.error(err);
121+
process.exitCode = 1;
122+
});

0 commit comments

Comments
 (0)