Skip to content

release: v0.21.6

release: v0.21.6 #17

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"