-
Notifications
You must be signed in to change notification settings - Fork 34
193 lines (165 loc) · 7.96 KB
/
smoke-opencode-linux.yml
File metadata and controls
193 lines (165 loc) · 7.96 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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
name: Smoke (opencode bring-up on Linux)
# Standalone diagnostic workflow. Verifies that `opencode serve` actually
# responds to HTTP after printing "server listening" on a fresh GitHub-hosted
# Linux runner. Runs in 1-3 minutes — fast enough to catch the regression
# pattern that historically broke our host e2e suite for ~20-40 minutes per
# CI run before timing out.
#
# Probes both bun's fetch AND curl independently, with each binding choice
# (127.0.0.1 vs 0.0.0.0) so we can attribute failures correctly:
#
# matrix entry | curl | fetch | meaning
# -----------------------------|------|-------|--------------------------------
# hostname=127.0.0.1, both ok | ✓ | ✓ | healthy, current host suite would pass
# hostname=127.0.0.1, curl OK | | |
# but fetch fails | ✓ | ✗ | bun fetch on linux has a localhost edge case
# hostname=0.0.0.0, both ok | ✓ | ✓ | binding all interfaces fixes the issue
# hostname=0.0.0.0, both fail | ✗ | ✗ | opencode-on-linux fundamental break
# any: opencode never prints | | |
# "server listening" | n/a | n/a | opencode failed to start (separate class)
#
# The workflow is intentionally NOT gated to anything in ci.yml or release.yml —
# we want clear, isolated signal independent of the broader pipeline.
on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
smoke:
name: opencode HTTP probe (${{ matrix.hostname }})
runs-on: ubuntu-latest
timeout-minutes: 8
strategy:
# Run both hostname choices even if one fails — we want full diagnostic
# coverage, not bail-on-first-failure.
fail-fast: false
matrix:
hostname: ["127.0.0.1", "0.0.0.0"]
steps:
- uses: actions/checkout@v5
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install opencode (1.15.4 pin matches host e2e)
run: |
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.15.4
echo "$HOME/.opencode/bin" >> "$GITHUB_PATH"
- name: Verify opencode binary
run: |
opencode --version
which opencode
- name: Probe opencode HTTP bring-up
env:
HOSTNAME: ${{ matrix.hostname }}
run: |
set -uo pipefail
# Random unprivileged port
PORT=$((30000 + RANDOM % 30000))
echo "::group::Spawn opencode serve --hostname $HOSTNAME --port $PORT"
# Isolated XDG dirs so the daemon does its first-run SQLite migration
# in a clean state — same as host e2e harness.
WORKDIR=$(mktemp -d)
export XDG_CONFIG_HOME="$WORKDIR/config"
export XDG_DATA_HOME="$WORKDIR/data"
export XDG_CACHE_HOME="$WORKDIR/cache"
mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME"
# Strip inherited NODE_ENV=test for the same reason host e2e does.
unset NODE_ENV
export ANTHROPIC_API_KEY="test-key-not-real"
# Tee stdout+stderr to files so we can inspect after probing.
opencode serve --hostname "$HOSTNAME" --port "$PORT" \
> "$WORKDIR/stdout.log" 2> "$WORKDIR/stderr.log" &
SERVE_PID=$!
echo "opencode serve pid: $SERVE_PID"
echo "::endgroup::"
# Wait up to 30s for opencode to print "listening" — that's the
# signal that Server.listen() returned.
for i in $(seq 1 150); do
if grep -q "opencode server listening on" "$WORKDIR/stdout.log" 2>/dev/null; then
break
fi
sleep 0.2
done
echo "::group::opencode stdout (post-listen window)"
cat "$WORKDIR/stdout.log"
echo "::endgroup::"
echo "::group::opencode stderr (post-listen window)"
cat "$WORKDIR/stderr.log"
echo "::endgroup::"
if ! grep -q "opencode server listening on" "$WORKDIR/stdout.log"; then
echo "::error::opencode never printed 'listening on' within 30s — process failed to start"
kill -TERM "$SERVE_PID" 2>/dev/null || true
exit 1
fi
# === Probe matrix: try both 127.0.0.1 and (if hostname=0.0.0.0)
# also localhost. Test BOTH curl and bun's fetch independently.
echo "::group::HTTP probes"
CURL_127=0
CURL_LOCALHOST=0
BUN_127=0
BUN_LOCALHOST=0
# curl 127.0.0.1
if curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/doc" > /dev/null 2>&1; then
CURL_127=1
echo " curl http://127.0.0.1:${PORT}/doc → OK"
else
echo " curl http://127.0.0.1:${PORT}/doc → FAIL"
fi
# curl localhost (different name resolution path)
if curl -fsS --max-time 5 "http://localhost:${PORT}/doc" > /dev/null 2>&1; then
CURL_LOCALHOST=1
echo " curl http://localhost:${PORT}/doc → OK"
else
echo " curl http://localhost:${PORT}/doc → FAIL"
fi
# bun fetch 127.0.0.1
if bun -e "const r = await fetch('http://127.0.0.1:${PORT}/doc'); console.log('status', r.status); if (!r.ok && r.status !== 404 && r.status !== 401) process.exit(1);" 2>&1; then
BUN_127=1
echo " bun fetch http://127.0.0.1:${PORT}/doc → OK"
else
echo " bun fetch http://127.0.0.1:${PORT}/doc → FAIL"
fi
# bun fetch localhost
if bun -e "const r = await fetch('http://localhost:${PORT}/doc'); console.log('status', r.status); if (!r.ok && r.status !== 404 && r.status !== 401) process.exit(1);" 2>&1; then
BUN_LOCALHOST=1
echo " bun fetch http://localhost:${PORT}/doc → OK"
else
echo " bun fetch http://localhost:${PORT}/doc → FAIL"
fi
echo "::endgroup::"
# Cleanup
kill -TERM "$SERVE_PID" 2>/dev/null || true
sleep 1
kill -KILL "$SERVE_PID" 2>/dev/null || true
# Summary in step output for easy table viewing
echo "## Probe results (hostname=${HOSTNAME})" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| probe | result |" >> "$GITHUB_STEP_SUMMARY"
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| curl 127.0.0.1 | $( [ "$CURL_127" = 1 ] && echo "✅" || echo "❌" ) |" >> "$GITHUB_STEP_SUMMARY"
echo "| curl localhost | $( [ "$CURL_LOCALHOST" = 1 ] && echo "✅" || echo "❌" ) |" >> "$GITHUB_STEP_SUMMARY"
echo "| bun 127.0.0.1 | $( [ "$BUN_127" = 1 ] && echo "✅" || echo "❌" ) |" >> "$GITHUB_STEP_SUMMARY"
echo "| bun localhost | $( [ "$BUN_LOCALHOST" = 1 ] && echo "✅" || echo "❌" ) |" >> "$GITHUB_STEP_SUMMARY"
# Fail the step if NEITHER probe reached the server. If curl
# works but fetch fails (or vice versa), surface the asymmetry
# via a clear annotation but DO NOT fail — that's diagnostic
# signal we want to capture, not a green/red gate.
ANY_OK=0
[ "$CURL_127" = 1 ] && ANY_OK=1
[ "$CURL_LOCALHOST" = 1 ] && ANY_OK=1
[ "$BUN_127" = 1 ] && ANY_OK=1
[ "$BUN_LOCALHOST" = 1 ] && ANY_OK=1
if [ "$ANY_OK" = 0 ]; then
echo "::error::Neither curl nor bun fetch could reach opencode (hostname=$HOSTNAME). This is a real bring-up failure."
exit 1
fi
if [ "$CURL_127" = 1 ] && [ "$BUN_127" = 0 ]; then
echo "::warning::curl reaches 127.0.0.1 but bun fetch does not — bun's HTTP client has a Linux loopback edge case."
fi
if [ "$BUN_LOCALHOST" = 1 ] && [ "$BUN_127" = 0 ]; then
echo "::warning::bun fetch works on 'localhost' but not '127.0.0.1' — bun's name resolution differs from raw IPv4 path."
fi
echo "Probe pass for hostname=$HOSTNAME"