Let the assistant configure the UTMStack connection in-conversation #3
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |