Skip to content

Commit b71e75b

Browse files
committed
ci: add curl|bash smoke test and old-CLI compat job (Phase 4)
- scripts/mock-server.py: minimal Python HTTP server that serves a fixed RemoteConfig, packages catalog, and a dry-run install script so CI can run the full curl|bash flow without a live server. - curl-bash-smoke job: builds the binary, starts the mock, runs `curl localhost/install | bash` with OPENBOOT_DRY_RUN=true on every PR. - cli-compat job: downloads the previous stable release binary and runs it against the mock to catch breaking API contract changes.
1 parent e75b8c0 commit b71e75b

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,96 @@ jobs:
9292
coverage.out
9393
coverage.html
9494
retention-days: 7
95+
96+
curl-bash-smoke:
97+
name: curl|bash smoke
98+
runs-on: macos-latest
99+
steps:
100+
- name: Checkout code
101+
uses: actions/checkout@v4
102+
103+
- name: Set up Go
104+
uses: actions/setup-go@v5
105+
with:
106+
go-version-file: "go.mod"
107+
108+
- name: Build
109+
run: make build
110+
111+
- name: Start mock server
112+
run: |
113+
python3 scripts/mock-server.py 18888 ./openboot &
114+
echo $! > /tmp/mock-pid
115+
for i in $(seq 1 20); do
116+
curl -sf http://localhost:18888/api/packages >/dev/null 2>&1 && break
117+
sleep 0.3
118+
done
119+
120+
- name: Run curl|bash install (dry-run)
121+
run: curl -fsSL http://localhost:18888/testuser/test-config/install | bash
122+
123+
- name: Stop mock server
124+
if: always()
125+
run: kill $(cat /tmp/mock-pid) 2>/dev/null || true
126+
127+
cli-compat:
128+
name: old-cli compat
129+
runs-on: macos-latest
130+
steps:
131+
- name: Checkout code
132+
uses: actions/checkout@v4
133+
134+
- name: Get previous release version
135+
id: prev
136+
env:
137+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138+
run: |
139+
PREV=$(curl -sf https://api.github.com/repos/openbootdotdev/openboot/releases \
140+
-H "Authorization: Bearer $GITHUB_TOKEN" | \
141+
python3 -c "
142+
import json, sys
143+
releases = json.load(sys.stdin)
144+
stable = [r for r in releases if not r.get('prerelease') and not r.get('draft')]
145+
if len(stable) >= 2:
146+
print(stable[1]['tag_name'])
147+
elif len(stable) == 1:
148+
print(stable[0]['tag_name'])
149+
else:
150+
sys.exit(1)
151+
" 2>/dev/null) || true
152+
echo "version=$PREV" >> $GITHUB_OUTPUT
153+
154+
- name: Download previous release binary
155+
if: steps.prev.outputs.version != ''
156+
run: |
157+
VER="${{ steps.prev.outputs.version }}"
158+
ARCH=$(uname -m)
159+
if [ "$ARCH" = "arm64" ]; then
160+
SUFFIX="darwin-arm64"
161+
else
162+
SUFFIX="darwin-amd64"
163+
fi
164+
curl -fsSL \
165+
"https://github.com/openbootdotdev/openboot/releases/download/${VER}/openboot-${SUFFIX}" \
166+
-o /tmp/openboot-prev
167+
chmod +x /tmp/openboot-prev
168+
169+
- name: Start mock server for compat test
170+
if: steps.prev.outputs.version != ''
171+
run: |
172+
python3 scripts/mock-server.py 18889 /tmp/openboot-prev &
173+
echo $! > /tmp/compat-mock-pid
174+
for i in $(seq 1 20); do
175+
curl -sf http://localhost:18889/api/packages >/dev/null 2>&1 && break
176+
sleep 0.3
177+
done
178+
179+
- name: Run previous binary against current mock
180+
if: steps.prev.outputs.version != ''
181+
run: |
182+
OPENBOOT_DRY_RUN=true OPENBOOT_API_URL=http://localhost:18889 \
183+
/tmp/openboot-prev -s -u testuser/test-config
184+
185+
- name: Stop compat mock server
186+
if: always()
187+
run: kill $(cat /tmp/compat-mock-pid) 2>/dev/null || true

scripts/mock-server.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python3
2+
"""Minimal HTTP mock server for OpenBoot CI smoke tests.
3+
4+
Serves fixed config/packages responses so the CLI can run dry-run installs
5+
in CI without a live openboot.dev instance.
6+
7+
Usage:
8+
python3 scripts/mock-server.py <port> <binary-path>
9+
10+
Example:
11+
python3 scripts/mock-server.py 18888 ./openboot
12+
"""
13+
14+
import json
15+
import sys
16+
from http.server import BaseHTTPRequestHandler, HTTPServer
17+
from urllib.parse import urlparse
18+
19+
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18888
20+
BINARY = sys.argv[2] if len(sys.argv) > 2 else "./openboot"
21+
22+
# Fixed RemoteConfig served at GET /<user>/<slug>/config
23+
CONFIG = {
24+
"username": "testuser",
25+
"slug": "test-config",
26+
"name": "Test Config",
27+
"preset": "minimal",
28+
"packages": [{"name": "git", "desc": "Version control"}],
29+
"casks": [],
30+
"taps": [],
31+
"npm": [],
32+
"dotfiles_repo": "",
33+
"post_install": [],
34+
"shell": None,
35+
"macos_prefs": [],
36+
}
37+
38+
# Fixed packages catalog served at GET /api/packages
39+
PACKAGES = {
40+
"packages": [
41+
{"name": "git", "desc": "Distributed version control", "category": "essential", "type": "cli"},
42+
{"name": "curl", "desc": "Transfer data via URLs", "category": "essential", "type": "cli"},
43+
]
44+
}
45+
46+
# Install script template — runs the local binary in dry-run silent mode
47+
INSTALL_SCRIPT = """\
48+
#!/bin/bash
49+
set -e
50+
main() {{
51+
if [ ! -t 0 ] && [ -e /dev/tty ]; then
52+
exec < /dev/tty || true
53+
fi
54+
export OPENBOOT_DRY_RUN=true
55+
export OPENBOOT_API_URL=http://localhost:{port}
56+
{binary} -s -u testuser/test-config
57+
exit 0
58+
}}
59+
main
60+
"""
61+
62+
63+
class MockHandler(BaseHTTPRequestHandler):
64+
def log_message(self, fmt, *args):
65+
pass # suppress per-request logs; server start is printed to stderr
66+
67+
def _send_json(self, data, status=200):
68+
body = json.dumps(data).encode()
69+
self.send_response(status)
70+
self.send_header("Content-Type", "application/json")
71+
self.send_header("Content-Length", str(len(body)))
72+
self.end_headers()
73+
self.wfile.write(body)
74+
75+
def _send_text(self, text, status=200):
76+
body = text.encode()
77+
self.send_response(status)
78+
self.send_header("Content-Type", "text/plain; charset=utf-8")
79+
self.send_header("Content-Length", str(len(body)))
80+
self.end_headers()
81+
self.wfile.write(body)
82+
83+
def do_GET(self):
84+
path = urlparse(self.path).path
85+
86+
if path in ("/testuser/test-config/config", "/test-user/test-config/config"):
87+
self._send_json(CONFIG)
88+
elif path == "/api/packages":
89+
self._send_json(PACKAGES)
90+
elif path in ("/testuser/test-config/install", "/test-user/test-config/install"):
91+
self._send_text(INSTALL_SCRIPT.format(port=PORT, binary=BINARY))
92+
else:
93+
self._send_json({"error": "not found"}, 404)
94+
95+
def do_POST(self):
96+
self._send_json({"error": "not found"}, 404)
97+
98+
99+
if __name__ == "__main__":
100+
server = HTTPServer(("localhost", PORT), MockHandler)
101+
sys.stderr.write(f"Mock server listening on http://localhost:{PORT}\n")
102+
server.serve_forever()

0 commit comments

Comments
 (0)