-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
173 lines (153 loc) · 7.05 KB
/
Copy pathMakefile
File metadata and controls
173 lines (153 loc) · 7.05 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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
GO ?= go
BIN := bin/spectackle
GORELEASER ?= go run github.com/goreleaser/goreleaser/v2@latest
.PHONY: all build vet test cover fuzz lint-specs smoke clean release-snapshot dev dev-stop dev-status releasenotes release-build
# Seconds of fuzzing per target in `make fuzz` (M4 linter hardening).
FUZZTIME ?= 10s
# Minimum total statement coverage (percent) enforced by `make cover`.
# Baseline at introduction: 75.0%; kept below to absorb noise, ratchet upward.
COVER_MIN ?= 70
# `make dev` target: address/pidfile/log for the resident dev server it
# manages, and the bounds on the readiness retry loop that proves the server
# actually answers before the target returns. Overridable like GO/BIN/etc
# above. Default port deliberately differs from any hand-started resident
# server (e.g. an orchestration server on 127.0.0.1:7411) so `make dev`
# never contends with one.
DEV_ADDR ?= 127.0.0.1:7412
DEV_PIDFILE ?= bin/dev.pid
DEV_LOG ?= bin/dev.log
DEV_READY_TRIES ?= 30
DEV_READY_SLEEP ?= 0.5
all: build vet test lint-specs smoke cover
build:
CGO_ENABLED=0 $(GO) build -o $(BIN) .
# dev rebuilds the binary and (re)starts it as a resident Streamable HTTP
# server, leaving exactly one instance running at DEV_ADDR with a stoppable
# pidfile at DEV_PIDFILE. Idempotent: running it twice in a row stops the
# server it started the first time before starting the replacement, so
# there is never a second process bound to the port. Readiness is proven
# with a real `spectackle call` against the running server (a bound socket
# answers before the index is warm, so a port probe would be a false
# positive) in a loop bounded by DEV_READY_TRIES/DEV_READY_SLEEP; if the
# server process dies (e.g. the port is already taken by something else)
# or never answers in time, the target fails loudly instead of hanging.
dev: build dev-stop
@mkdir -p $(dir $(DEV_PIDFILE))
@rm -f $(DEV_LOG)
@./$(BIN) serve -root . -http $(DEV_ADDR) -pidfile $(DEV_PIDFILE) >$(DEV_LOG) 2>&1 & \
pid=$$!; \
i=0; \
while [ $$i -lt $(DEV_READY_TRIES) ]; do \
if ! kill -0 $$pid 2>/dev/null; then \
echo "dev: server process exited before becoming ready on $(DEV_ADDR); log:" >&2; \
cat $(DEV_LOG) >&2; \
exit 1; \
fi; \
if ./$(BIN) call -http $(DEV_ADDR) state >/dev/null 2>&1; then \
echo "dev: resident server ready on $(DEV_ADDR) (pid $$pid, pidfile $(DEV_PIDFILE))"; \
exit 0; \
fi; \
i=$$((i + 1)); \
sleep $(DEV_READY_SLEEP); \
done; \
echo "dev: server on $(DEV_ADDR) did not answer a real call after $(DEV_READY_TRIES) tries; log:" >&2; \
cat $(DEV_LOG) >&2; \
kill $$pid 2>/dev/null; \
exit 1
# dev-stop stops the resident dev server tracked by DEV_PIDFILE, if any.
# Semantics match main.go's pidfile handling: a live PID is
# stopped (SIGTERM, falling back to SIGKILL if it won't exit) and its
# pidfile removed; a stale pidfile (recorded PID no longer running -- serve
# itself removes the file on clean shutdown, so a leftover file always
# means a dead process) is cleared rather than left to wedge the next
# `serve -pidfile`, which refuses to start while the file exists. Doing
# nothing when no pidfile exists is success, not failure, so a fresh clone
# or a repeated stop is a no-op.
dev-stop:
@if [ -f $(DEV_PIDFILE) ]; then \
pid=$$(cat $(DEV_PIDFILE) 2>/dev/null); \
if [ -n "$$pid" ] && kill -0 "$$pid" 2>/dev/null; then \
echo "dev-stop: stopping resident server (pid $$pid)..."; \
kill "$$pid" 2>/dev/null; \
i=0; \
while kill -0 "$$pid" 2>/dev/null && [ $$i -lt $(DEV_READY_TRIES) ]; do \
i=$$((i + 1)); \
sleep $(DEV_READY_SLEEP); \
done; \
if kill -0 "$$pid" 2>/dev/null; then \
echo "dev-stop: pid $$pid did not exit in time; sending SIGKILL" >&2; \
kill -9 "$$pid" 2>/dev/null || true; \
fi; \
rm -f $(DEV_PIDFILE); \
echo "dev-stop: stopped"; \
else \
echo "dev-stop: stale pidfile $(DEV_PIDFILE) (pid $$pid not running); clearing"; \
rm -f $(DEV_PIDFILE); \
fi; \
else \
echo "dev-stop: not running"; \
fi
# dev-status reports whether the DEV_PIDFILE-tracked resident dev server is
# running, without changing anything. Exits non-zero when not running (a
# stale pidfile counts as not running), so it composes in scripts.
dev-status:
@if [ -f $(DEV_PIDFILE) ]; then \
pid=$$(cat $(DEV_PIDFILE) 2>/dev/null); \
if [ -n "$$pid" ] && kill -0 "$$pid" 2>/dev/null; then \
echo "dev-status: running (pid $$pid, $(DEV_ADDR), pidfile $(DEV_PIDFILE))"; \
else \
echo "dev-status: not running (stale pidfile $(DEV_PIDFILE), pid $$pid gone)"; \
exit 1; \
fi; \
else \
echo "dev-status: not running"; \
exit 1; \
fi
vet:
$(GO) vet ./...
test:
$(GO) test -race ./...
fuzz:
$(GO) test ./internal/ears/ -fuzz=FuzzLintSentence -fuzztime=$(FUZZTIME)
$(GO) test ./internal/ears/ -fuzz=FuzzParseRules -fuzztime=$(FUZZTIME)
$(GO) test ./internal/ears/ -fuzz=FuzzStripFrontMatter -fuzztime=$(FUZZTIME)
# bench meters the MCP text surface: a scripted all-state lifecycle over a
# generated fixture, bytes and estimated tokens per tool, validity-gated.
# BENCH_AGAINST=path/to/other/binary A/Bs two builds.
bench: build
@if [ -n "$(BENCH_AGAINST)" ]; then ./$(BIN) bench -against "$(BENCH_AGAINST)"; else ./$(BIN) bench; fi
cover:
@mkdir -p bin
$(GO) test -coverprofile=bin/cover.out ./...
@$(GO) tool cover -func=bin/cover.out | awk -v min=$(COVER_MIN) '\
/^total:/ { sub(/%/, "", $$3); \
if ($$3 + 0 < min) { printf "coverage %s%% below COVER_MIN %s%%\n", $$3, min; exit 1 } \
else { printf "coverage %s%% (min %s%%)\n", $$3, min } }'
lint-specs: build
./$(BIN) lint .
# Pipe an MCP handshake + tools/list into the stdio server and check that the
# tool surface is present (SPX-ARC-001: stdout must be pure JSON-RPC).
smoke: build
@(printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; sleep 1) \
| ./$(BIN) serve -root . 2>/dev/null \
| grep -q '"draft"' && echo "smoke: OK (tools listed)" || (echo "smoke: FAILED"; exit 1)
# Build all release targets locally without publishing, to sanity-check the
# goreleaser config (.goreleaser.yaml) before tagging. Uses `go run` so a
# goreleaser binary need not be pre-installed; override GORELEASER=goreleaser
# to use a local install instead.
release-snapshot:
$(GORELEASER) release --snapshot --clean --skip=publish
clean:
rm -rf bin
# Release notes derived from the journal archive tombstones (T-01KYGCJ6):
# SINCE is the previous release tag's instant. The human fronts the output
# with an intro; the generated body IS the change log.
releasenotes: build
./$(BIN) releasenotes -root . -since $(SINCE)
# A version-stamped build: VERSION is normally the tag being cut. The stamp
# lands in the MCP handshake and the CLI via internal/mcpserver.Version.
release-build:
$(GO) build -ldflags "-X github.com/jxsl13/spectackle/internal/mcpserver.Version=$(VERSION)" -o $(BIN) .