-
Notifications
You must be signed in to change notification settings - Fork 0
129 lines (121 loc) · 4.99 KB
/
Copy pathci.yml
File metadata and controls
129 lines (121 loc) · 4.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [20, 22]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm run build
- run: npm test
smoke:
# The build passing says nothing about whether the server actually starts
# and lists its tools, which is the failure a user would actually hit.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- name: Server starts and lists every tool
env:
THREADS_ACCESS_TOKEN: ci-placeholder
THREADS_TOKEN_STORE: /dev/null
run: |
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| node dist/index.js 2>/dev/null \
| node -e '
let raw = "";
process.stdin.on("data", c => raw += c);
process.stdin.on("end", async () => {
const listed = raw.trim().split("\n").map(l => JSON.parse(l)).find(m => m.id === 2);
const tools = listed?.result?.tools ?? [];
console.log(`tools: ${tools.length}`);
// Compared against the source, not a number typed here: a
// literal turns every tool added into a red build.
const { ALL_TOOLS } = await import("./dist/tools/index.js");
const expected = ALL_TOOLS.length;
if (tools.length !== expected) {
console.error(`handshake exposed ${tools.length} tools, ALL_TOOLS has ${expected}`);
process.exit(1);
}
const unannotated = tools.filter(t => t.annotations?.readOnlyHint === undefined);
if (unannotated.length) {
console.error("unannotated: " + unannotated.map(t => t.name).join(", "));
process.exit(1);
}
});
'
- name: Read-only mode hides every write
env:
THREADS_ACCESS_TOKEN: ci-placeholder
THREADS_TOKEN_STORE: /dev/null
THREADS_READ_ONLY: "1"
run: |
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| node dist/index.js 2>/dev/null \
| node -e '
let raw = "";
process.stdin.on("data", c => raw += c);
process.stdin.on("end", () => {
const listed = raw.trim().split("\n").map(l => JSON.parse(l)).find(m => m.id === 2);
const writes = listed.result.tools.filter(t => !t.annotations.readOnlyHint);
if (writes.length) {
console.error("writes exposed in read-only mode: " + writes.map(t => t.name).join(", "));
process.exit(1);
}
console.log(`read-only tools: ${listed.result.tools.length}`);
});
'
- name: A public write refuses without confirm
env:
THREADS_ACCESS_TOKEN: ci-placeholder
THREADS_TOKEN_STORE: /dev/null
run: |
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"create_post","arguments":{"text":"ci"}}}' \
| node dist/index.js 2>/dev/null \
| node -e '
let raw = "";
process.stdin.on("data", c => raw += c);
process.stdin.on("end", () => {
const called = raw.trim().split("\n").map(l => JSON.parse(l)).find(m => m.id === 3);
const text = called?.result?.content?.[0]?.text ?? "";
if (!called?.result?.isError || !text.includes("confirm: true")) {
console.error("create_post did not refuse without confirm");
process.exit(1);
}
console.log("write guard holds");
});
'
docker:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: docker build -t threads-mcp-cli:ci .