Repository navigation
Expand file tree
/
Copy pathci-smoke-binary-source-github.yml
More file actions
322 lines (298 loc) · 15.1 KB
/
Copy pathci-smoke-binary-source-github.yml
File metadata and controls
322 lines (298 loc) · 15.1 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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
name: smoke-binary-source-github
# Verifies that the BINARY_SOURCE=github auto-update path stays end-to-end
# functional. #646 (v0.65.9 → v0.65.10) shipped without this CI smoke job in
# place and the hosted-SaaS deploy of v0.65.9 surfaced an agent-rejected
# download URL only after the prod rollout. This job exercises the same
# path on every PR that touches the relevant code so the regression class
# fails CI instead of the fleet.
#
# What it asserts:
# - API boots cleanly in BINARY_SOURCE=github mode with a published
# release tag.
# - GET /agent-versions/<version>/download returns 200 with a
# SERVER-RELATIVE url (matching PUBLIC_API_URL host), NOT the
# canonical github.com URL. That's the #646 fix and the regression
# would re-introduce auto-update breakage on hosted SaaS.
on:
pull_request:
paths:
- "apps/api/src/services/binarySync.ts"
- "apps/api/src/services/releaseArtifactManifest.ts"
- "apps/api/src/routes/agentVersions.ts"
- "apps/api/src/routes/agents/download.ts"
- "apps/api/src/services/binarySource.ts"
- "apps/api/migrations/**"
- ".github/workflows/ci-smoke-binary-source-github.yml"
push:
branches: [main]
env:
PNPM_VERSION: '10.34.5'
permissions:
contents: read
jobs:
smoke:
name: BINARY_SOURCE=github download path
runs-on: ubuntu-latest
timeout-minutes: 10
services:
# pgvector-enabled image, though this job no longer NEEDS it: built-in
# extensions load only when their enable flag is set, and this job
# leaves BREEZE_WORKSPACE_ENABLED unset, so ee/workspace/migrations/
# *.sql (CREATE EXTENSION vector) never runs. Kept to match the image
# .env.example ships, so the boot proved here is the one self-hosters
# run — a stock postgres:16-alpine would boot identically.
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: breeze
POSTGRES_PASSWORD: breeze
POSTGRES_DB: breeze
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U breeze"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version-file: .node-version
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build API
run: pnpm --filter=@breeze/api build
# NOTE: no `ee/workspace` web build here, deliberately — see the same
# note in ci-smoke-binary-source-local.yml. This job leaves
# BREEZE_WORKSPACE_ENABLED unset, so the built-in never loads and its
# dist/web is never required.
- name: Boot API in BINARY_SOURCE=github mode
env:
# Authenticate binarySync's github API call so we don't hit the
# 60/hour unauthenticated rate limit on the shared CI runner IP.
GITHUB_TOKEN: ${{ github.token }}
BINARY_SOURCE: github
# Use the most recent published release whose manifest format is
# known to be compatible with the current API code. Bump this when
# cutting a new release if the manifest schema changes.
BREEZE_VERSION: "0.65.8"
DATABASE_URL: postgres://breeze:breeze@localhost:5432/breeze
DATABASE_URL_APP: postgres://breeze_app:breeze@localhost:5432/breeze
BREEZE_APP_DB_PASSWORD: breeze
POSTGRES_PASSWORD: breeze
REDIS_URL: redis://localhost:6379
# The CI Redis service has no password; #909 made unauthenticated
# Redis fail-closed in self-hosted production (IS_HOSTED=false
# below). This override acknowledges the CI-only sandbox posture
# so resolveRedisUrl() warns instead of throwing. Without it the
# smoke step exits 1 at boot before the binary-download path is
# ever exercised. See #969.
BREEZE_ALLOW_UNAUTH_REDIS: "true"
NODE_ENV: production
PUBLIC_APP_URL: http://localhost:3001
# PUBLIC_API_URL drives the server-relative URL rewrite in
# getServerOriginForDownloadResponse — the actual thing #646 fixes.
PUBLIC_API_URL: http://localhost:3001
# Production-strict env that the API requires at boot post-#568.
JWT_SECRET: ci-smoke-bsg-k7x9Qm4pR2vL8nW5jT3yF6hB0cA1dE4gI7
# Partner reconstruction API cursor signing key (32 random bytes,
# canonical base64) — required at production boot post-#2506.
PARTNER_API_CURSOR_SIGNING_KEY: ZYPDfyEGlkK3+NOZT1SlzbzAxpO/3vQRqXTA1HtPLMc=
APP_ENCRYPTION_KEY: e9367714341ea19491ff7cd095d1a97e15ae3f7962416723ecfb29a89ed71eb9
MFA_ENCRYPTION_KEY: e86f8b88f2c058d428de55c4abec1b50a3cd960a7205dffdb65be8130abbb777
ENROLLMENT_KEY_PEPPER: ci-smoke-bsg-enrollment-pepper-Hy4Bz9Mp2Wq6Tv0Lk3
MFA_RECOVERY_CODE_PEPPER: ci-smoke-bsg-mfa-recovery-pepper-Nq8Zx2Vm5Tr9Lp1
AGENT_ENROLLMENT_SECRET: ci-smoke-bsg-agent-enrollment-Hy4Bz9Mp2Wq6Tv0Lk3
ENROLLMENT_SECRET_ENFORCEMENT_MODE: warn
IS_HOSTED: "false"
# Required in production by the Wave 3 live-authorization epoch
# rollout — compat until the fleet drains version-one writers.
EVENT_PERMISSION_EPOCH_MODE: compat
# Wave 4 remote-access runtime config (#2842) — required at
# production boot by getRemoteWsRuntimeConfig(). Values match the
# CI stack in ci.yml; irrelevant to the binary-download path this
# smoke exercises, but the API refuses to start without them.
REMOTE_ACCESS_ADMISSION_MODE: open
REMOTE_WS_AUTH_MODE: post_upgrade
REMOTE_WS_REDIS_TOPOLOGY: standalone-single-primary
TRUSTED_PROXY_CIDRS: 127.0.0.1/32
CORS_ALLOWED_ORIGINS: http://localhost:3001
TRUST_PROXY_HEADERS: "false"
BREEZE_BOOTSTRAP_ADMIN_EMAIL: smoke-admin@example.test
BREEZE_BOOTSTRAP_ADMIN_PASSWORD: ci-smoke-bsg-bootstrap-admin-Hy4Bz9Mp2Wq6Tv0Lk3
# Production LanternOps trust root for release manifest signatures.
# This is the public half of the keypair the release workflow signs
# with; it's safe to commit (public keys are not secret).
RELEASE_ARTIFACT_MANIFEST_PUBLIC_KEYS: "yzx8ftmcls6uBetFC5SYnZhBo+cbur3IX50TbBthTso="
run: |
pnpm --filter=@breeze/api start &
API_PID=$!
echo "Waiting for /health/ready..."
for i in $(seq 1 60); do
if curl -sf http://localhost:3001/health/ready >/dev/null 2>&1; then
echo "API healthy after ${i} polls."
break
fi
if ! kill -0 "$API_PID" 2>/dev/null; then
echo "API process exited before becoming healthy."
exit 1
fi
sleep 2
done
curl -sf http://localhost:3001/health/ready || { echo "API never became healthy"; kill "$API_PID" 2>/dev/null; exit 1; }
echo "API_PID=$API_PID" >> "$GITHUB_ENV"
- name: Verify download endpoint returns 200 with server-relative URL (#646 regression check)
run: |
STATUS=$(curl -s -o /tmp/dl.json -w '%{http_code}' \
'http://localhost:3001/api/v1/agent-versions/0.65.8/download?platform=linux&arch=amd64&component=agent')
echo "Download endpoint returned: ${STATUS}"
cat /tmp/dl.json | jq . || cat /tmp/dl.json
if [ "$STATUS" != "200" ]; then
echo "Expected 200 — github-mode download endpoint should serve published manifests."
exit 1
fi
# The whole point of #646: the URL handed to the agent must be on
# our own origin, NOT github.com. Without this, the agent's
# downloadFromURL host check rejects with "download URL host
# \"github.com\" does not match server".
url=$(jq -er '.url' /tmp/dl.json)
echo "downloadUrl returned: ${url}"
case "$url" in
http://localhost:3001/api/v1/agents/download/*)
echo "OK — server-relative download URL"
;;
*)
echo "REGRESSION (#646): expected a server-relative URL under http://localhost:3001/api/v1/agents/download/, got: ${url}"
exit 1
;;
esac
# Manifest + signature should still be present (signed by the
# LanternOps trust root via GitHub release artifacts).
jq -e '.manifest != null and (.manifest | length) > 0' /tmp/dl.json
jq -e '.manifestSignature != null and (.manifestSignature | length) > 0' /tmp/dl.json
- name: Verify the returned URL is version-pinned and 302s to THAT release (#5159)
run: |
# #5159: the URL /agent-versions/:version/download hands back must
# resolve to the SAME release its checksum came from. Before the fix
# it was versionless and the route served whatever was promoted, so
# a pinned/unpromoted version got another release's bytes and the
# agent looped forever on a checksum mismatch it could never clear.
url=$(jq -er '.url' /tmp/dl.json)
case "$url" in
*"version=0.65.8"*) echo "OK — URL is pinned to the requested version" ;;
*)
echo "REGRESSION (#5159): expected ?version=0.65.8 in the returned URL, got: ${url}"
exit 1
;;
esac
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$url")
if [ "$STATUS" != "302" ]; then
echo "Expected 302 from the version-pinned download URL; got ${STATUS}"
exit 1
fi
LOCATION=$(curl -s -o /dev/null -w '%{redirect_url}' "$url")
echo "Pinned redirect target: ${LOCATION}"
case "$LOCATION" in
*"/releases/download/v0.65.8/"*)
echo "OK — pinned URL redirects to the v0.65.8 release asset"
;;
*)
echo "REGRESSION (#5159): pinned URL served a different release: ${LOCATION}"
exit 1
;;
esac
# An unregistered version must 404, never fall back to the promoted
# release — silent substitution is the bug being fixed.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
'http://localhost:3001/api/v1/agents/download/linux/amd64?version=0.0.0-not-registered')
if [ "$STATUS" != "404" ]; then
echo "Expected 404 for an unregistered ?version=; got ${STATUS}"
exit 1
fi
- name: Verify /agents/download proxy route 302s to github
run: |
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
'http://localhost:3001/api/v1/agents/download/linux/amd64')
if [ "$STATUS" != "302" ]; then
echo "Expected 302 redirect from /agents/download/:os/:arch in github mode; got ${STATUS}"
exit 1
fi
LOCATION=$(curl -s -o /dev/null -w '%{redirect_url}' \
'http://localhost:3001/api/v1/agents/download/linux/amd64')
echo "Redirect target: ${LOCATION}"
# GitHub returns the org slug lowercased in 302 Location headers
# even though the canonical case is "LanternOps". Compare case-
# insensitively to be robust against either form.
shopt -s nocasematch
case "$LOCATION" in
https://github.com/lanternops/breeze/releases/download/*)
echo "OK — redirect points at github releases"
;;
*)
echo "Expected redirect to github.com/LanternOps/breeze/releases/...; got ${LOCATION}"
exit 1
;;
esac
shopt -u nocasematch
- name: Reproduce the #5159 divergence against a real database
env:
PGPASSWORD: breeze
run: |
# Everything above runs with ONE registered release, so the pinned
# and promoted versions coincide and the original bug cannot appear.
# Force them apart the way AGENT_AUTO_PROMOTE=false + an org
# agentVersionPins pilot does in production: promote a DIFFERENT
# version than the one the caller pins.
PSQL="psql -h localhost -U breeze -d breeze -v ON_ERROR_STOP=1"
$PSQL -c "INSERT INTO agent_versions (version, platform, architecture, component, download_url, checksum, file_size, edition, is_latest) SELECT '0.65.7', platform, architecture, component, download_url, checksum, file_size, edition, false FROM agent_versions WHERE component='agent' AND platform='linux' AND architecture='amd64' AND version='0.65.8' ON CONFLICT DO NOTHING;"
$PSQL -c "UPDATE agent_versions SET is_latest = (version='0.65.7') WHERE component='agent' AND platform='linux' AND architecture='amd64';"
# Guard the fixture itself: if the seed matched no row, the two
# assertions below would compare identical versions and pass
# vacuously.
PROMOTED=$(psql -h localhost -U breeze -d breeze -tAc \
"SELECT version FROM agent_versions WHERE component='agent' AND platform='linux' AND architecture='amd64' AND is_latest")
if [ "$PROMOTED" != "0.65.7" ]; then
echo "Fixture failed: expected 0.65.7 promoted, got '${PROMOTED}'"
exit 1
fi
# The versionless route follows the promotion.
PROMOTED_LOC=$(curl -s -o /dev/null -w '%{redirect_url}' \
'http://localhost:3001/api/v1/agents/download/linux/amd64')
echo "versionless -> ${PROMOTED_LOC}"
case "$PROMOTED_LOC" in
*"/releases/download/v0.65.7/"*) echo "OK — versionless serves the promoted release" ;;
*) echo "Expected the promoted v0.65.7 asset; got ${PROMOTED_LOC}"; exit 1 ;;
esac
# THE BUG: a pinned, deliberately UNPROMOTED version must still get
# its own bytes. Before the fix this returned the v0.65.7 asset
# while the agent held the v0.65.8 checksum, and the device sat in
# "Updating" forever.
PINNED_LOC=$(curl -s -o /dev/null -w '%{redirect_url}' \
'http://localhost:3001/api/v1/agents/download/linux/amd64?version=0.65.8')
echo "pinned -> ${PINNED_LOC}"
case "$PINNED_LOC" in
*"/releases/download/v0.65.8/"*) echo "OK — pinned, unpromoted version serves its OWN release" ;;
*) echo "REGRESSION (#5159): pinned v0.65.8 served ${PINNED_LOC}"; exit 1 ;;
esac
- name: Stop API
if: always()
run: |
if [ -n "${API_PID:-}" ]; then
kill "$API_PID" 2>/dev/null || true
fi