-
Notifications
You must be signed in to change notification settings - Fork 0
150 lines (136 loc) · 5.6 KB
/
Copy pathrelease.yml
File metadata and controls
150 lines (136 loc) · 5.6 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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Tag to build (for a dry run)"
required: false
permissions:
contents: write
jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { runner: macos-14, target: darwin-arm64, archive: tar.gz }
- { runner: macos-15-intel, target: darwin-amd64, archive: tar.gz }
- { runner: ubuntu-22.04, target: linux-amd64, archive: tar.gz }
- { runner: ubuntu-24.04-arm, target: linux-arm64, archive: tar.gz }
- { runner: windows-2022, target: windows-amd64, archive: zip }
# Windows on ARM: Parallels/Apple Silicon VMs and Surface-class
# hardware. Verified working on a Windows 11 ARM64 VM.
- { runner: windows-11-arm, target: windows-arm64, archive: zip }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
# Wheels only: a source build of a transitive dependency (cryptography)
# is slow and needs a Rust toolchain.
pip install --only-binary=:all: . pyinstaller
- name: Build
shell: bash
run: pyinstaller --clean --noconfirm utmstack-mcp.spec
- name: Smoke test — real MCP handshake
shell: bash
run: |
set -eu
# A bare `[ ... ] && VAR=...` returns non-zero when the test fails,
# which aborts the step under `set -eu` on non-Windows runners.
if [ "${{ runner.os }}" = "Windows" ]; then
BIN="dist/utmstack-mcp/utmstack-mcp.exe"
else
BIN="dist/utmstack-mcp/utmstack-mcp"
fi
"$BIN" --version
# Drive an actual initialize + tools/list over stdio and assert that
# every tool enumerates. A binary that cannot list its tools is broken
# regardless of whether it starts.
python - "$BIN" <<'PY'
import json, subprocess, sys
binary = sys.argv[1]
p = subprocess.Popen([binary], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True, bufsize=1)
def send(o): p.stdin.write(json.dumps(o) + "\n"); p.stdin.flush()
send({"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"ci","version":"1"}}})
init = json.loads(p.stdout.readline())
assert init["result"]["serverInfo"]["name"] == "utmstack", init
send({"jsonrpc":"2.0","method":"notifications/initialized"})
send({"jsonrpc":"2.0","id":2,"method":"tools/list"})
tools = json.loads(p.stdout.readline())["result"]["tools"]
print(f"enumerated {len(tools)} tools")
assert len(tools) >= 40, f"expected >=40 tools, got {len(tools)}"
for required in ("ping", "search_alerts", "run_agent_command", "list_servers"):
assert any(t["name"] == required for t in tools), f"missing tool: {required}"
p.stdin.close(); p.terminate()
print("handshake OK")
PY
- name: Security regression tests
shell: bash
run: |
python - <<'PY'
import json
from utmstack_mcp.server import (_can_run_ok, _stomp_frame, _valid_hostname,
_clamp_size, _check_bulk_ids)
# The can-run-command gate must deny on every error path: _req returns
# {"error": true, ...} on failure, so a substring check would treat an
# outage as permission to execute.
assert _can_run_ok("true") is True
assert _can_run_ok("false") is False
assert _can_run_ok(json.dumps({"error": True})) is False
assert _can_run_ok("<html>502</html>") is False
# STOMP header injection via hostname.
assert _valid_hostname("HOST-1") and not _valid_hostname("a\nb")
try:
_stomp_frame("SUB", "destination:/x\ndestination:/y")
raise AssertionError("STOMP injection was not rejected")
except ValueError:
pass
# Blast-radius caps.
assert _clamp_size(999999) == 1000
assert _check_bulk_ids(["x"] * 200) is not None
print("security regressions OK")
PY
- name: Package
shell: bash
run: |
set -eu
mkdir -p out
if [ "${{ matrix.archive }}" = "zip" ]; then
(cd dist && 7z a -tzip "../out/utmstack-mcp-${{ matrix.target }}.zip" utmstack-mcp)
else
env COPYFILE_DISABLE=1 tar --no-xattrs -czf "out/utmstack-mcp-${{ matrix.target }}.tar.gz" -C dist utmstack-mcp
fi
ls -lh out/
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: out/*
release:
needs: build
runs-on: ubuntu-22.04
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v4
with: { path: artifacts }
- name: Collect and checksum
run: |
mkdir -p out
find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' \) -exec cp {} out/ \;
cd out && sha256sum * > checksums.txt
cat checksums.txt
- uses: softprops/action-gh-release@v2
with:
files: out/*
generate_release_notes: true