Skip to content

Let the assistant configure the UTMStack connection in-conversation #3

Let the assistant configure the UTMStack connection in-conversation

Let the assistant configure the UTMStack connection in-conversation #3

Workflow file for this run

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",
"configure_server", "remove_server"):
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