-
Notifications
You must be signed in to change notification settings - Fork 3
449 lines (385 loc) Β· 17.1 KB
/
nodejs-frontend-preview.yml
File metadata and controls
449 lines (385 loc) Β· 17.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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
name: NodeJS Frontend Preview
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
inputs:
timeout_minutes:
description: 'Preview timeout in minutes'
required: false
default: '5'
type: string
jobs:
preview:
runs-on: ubuntu-latest
timeout-minutes: 20 # small buffer above 10-minute tunnel lifetime
env:
PREVIEW_TIMEOUT_MINUTES: ${{ github.event.inputs.timeout_minutes || '5' }}
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
MADE_HOME: ${{ format('{0}/workspace/', github.workspace) }}
MADE_WORKSPACE_HOME: ${{ format('{0}/workspace/', github.workspace) }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
- name: Authenticate GitHub CLI
env:
GH_AUTH_TOKEN: ${{ secrets.GH_AUTH_TOKEN }}
run: |
echo "$GH_AUTH_TOKEN" | gh auth login --with-token
gh auth status
- name: Verify triggering actor is repo admin
id: admin
env:
REPOSITORY: ${{ github.repository }}
TRIGGER_ACTOR: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login || github.triggering_actor }}
run: |
permission=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/$REPOSITORY/collaborators/$TRIGGER_ACTOR/permission" \
--jq '.permission // ""' 2>/dev/null || echo '')
if [[ "$permission" == "admin" ]]; then
echo "is_admin=true" >> "$GITHUB_OUTPUT"
else
echo "is_admin=false" >> "$GITHUB_OUTPUT"
fi
- name: Require admin privileges for preview
if: ${{ steps.admin.outputs.is_admin != 'true' }}
env:
TRIGGER_ACTOR: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login || github.triggering_actor }}
run: |
echo "::error::Preview deployment requires repository admin privileges. Triggering actor: $TRIGGER_ACTOR"
exit 1
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Install latest ngrok CLI (v4)
run: |
echo "π§ Installing official ngrok CLI (v4)..."
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \
sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \
sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt-get update -y
sudo apt-get install -y ngrok
echo "β
Installed $(ngrok version)"
- name: Detect Node.js frontend directory
id: detect
run: |
echo "π Searching for Node.js frontend in this PR branch..."
# Look for frontend specifically (exclude root package.json)
frontend_package=""
if [ -f "packages/frontend/package.json" ]; then
# Check if it has vite or other frontend dev script
if jq -e '.scripts.dev and (.dependencies.react or .devDependencies.vite)' packages/frontend/package.json > /dev/null 2>&1; then
frontend_package="packages/frontend"
fi
fi
if [ -z "$frontend_package" ]; then
echo "::warning::No frontend package detected in packages/frontend. This is expected for backend-only changes."
echo "πͺ Skipping preview deployment β no frontend present."
echo "frontend_found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "β
Found frontend package at: $frontend_package"
echo "frontend_dir=$frontend_package" >> $GITHUB_OUTPUT
echo "frontend_found=true" >> $GITHUB_OUTPUT
- name: Set up Python
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install UV
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: curl -LsSf https://astral.sh/uv/install.sh | sh
# ------------------------------------------------------
# Install OpenCode (binary install)
# ------------------------------------------------------
- name: Install opencode
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: |
set -euo pipefail
INSTALL_DIR="$HOME/.opencode/bin"
mkdir -p "$INSTALL_DIR"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
ARCHIVE_PATH="$TMPDIR/opencode.tar.gz"
DOWNLOAD_URL="https://github.com/anomalyco/opencode/releases/latest/download/opencode-linux-x64.tar.gz"
# Robust download function with retry logic and fallback
download_with_retry() {
local url="$1"
local output="$2"
local max_attempts=3
echo "π Downloading OpenCode CLI from GitHub releases..."
# Try curl with retries
for attempt in $(seq 1 $max_attempts); do
echo "π₯ Attempt $attempt/$max_attempts using curl..."
if curl --connect-timeout 30 --max-time 120 --retry-connrefused \
--fail --silent --show-error --location \
"$url" -o "$output"; then
echo "β
Download successful with curl on attempt $attempt"
return 0
else
local exit_code=$?
echo "β curl attempt $attempt failed (exit code: $exit_code)"
if [ $attempt -lt $max_attempts ]; then
local delay=$((attempt * 5))
echo "β³ Waiting ${delay}s before retry..."
sleep $delay
fi
fi
done
# Fallback to wget if curl failed
echo "π Trying fallback method with wget..."
if wget --timeout=30 --tries=2 --waitretry=5 -q "$url" -O "$output"; then
echo "β
Download successful with wget fallback"
return 0
else
echo "β wget fallback also failed"
return 1
fi
}
# Download with robust retry logic
if ! download_with_retry "$DOWNLOAD_URL" "$ARCHIVE_PATH"; then
echo "π₯ Failed to download OpenCode CLI after all retry attempts and fallbacks"
echo "π This is likely a temporary CDN issue. Please retry the workflow."
exit 1
fi
# Validate download
if [ ! -f "$ARCHIVE_PATH" ] || [ ! -s "$ARCHIVE_PATH" ]; then
echo "β Downloaded file is missing or empty"
exit 1
fi
file_size=$(stat -f%z "$ARCHIVE_PATH" 2>/dev/null || stat -c%s "$ARCHIVE_PATH" 2>/dev/null || echo "0")
if [ "$file_size" -lt 100000 ]; then
echo "β Downloaded file seems too small ($file_size bytes), likely corrupted"
exit 1
fi
echo "β
Downloaded file validated ($file_size bytes)"
# Extract and install
tar -xzf "$ARCHIVE_PATH" -C "$TMPDIR"
install "$TMPDIR/opencode" "$INSTALL_DIR/opencode"
echo "$INSTALL_DIR" >> "$GITHUB_PATH"
# ------------------------------------------------------
# Configure OpenCode
# ------------------------------------------------------
- name: Configure opencode
run: |
mkdir -p "$HOME/.config/opencode"
cp .github/opencode/config.json "$HOME/.config/opencode/config.json"
check_file() {
local path="$1"
local label="$2"
if [ -s "$path" ]; then
echo "β
$label is present and non-empty ($path)"
else
echo "β $label is missing or empty ($path)"
exit 1
fi
}
check_file "$HOME/.config/opencode/config.json" "config.json"
- name: Prepare opencode skills directory
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: |
mkdir -p "$HOME/.opencode/skills"
- name: Run opencode success check
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
continue-on-error: true
run: "opencode run 'Say: β
SUCCESS!'"
- name: Install dependencies
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: make install
- name: Debug frontend dependencies
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: |
echo "π Debugging frontend setup..."
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
echo "Frontend package.json:"
cat packages/frontend/package.json
echo ""
echo "Checking if vite is available:"
cd packages/frontend && npx vite --version || echo "β Vite not found"
echo "Checking node_modules:"
ls -la packages/frontend/node_modules/.bin/ | grep vite || echo "β No vite binary found"
- name: Start MADE services (frontend + backend)
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: |
echo "π Starting MADE services with make run..."
# Start both frontend and backend using make run
nohup make run > services.log 2>&1 &
SERVICES_PID=$!
echo "Services started with PID $SERVICES_PID"
echo "β³ Waiting for services to become available..."
# Wait a bit and check if the process is still running
sleep 5
if ! kill -0 $SERVICES_PID 2>/dev/null; then
echo "β Services process died unexpectedly!"
echo "π Full services.log output:"
cat services.log
exit 1
fi
# Wait for backend API
for i in {1..30}; do
if curl -f http://localhost:3000/api/repositories > /dev/null 2>&1; then
echo "β
Backend API is ready on port 3000"
break
fi
if [ $i -eq 30 ]; then
echo "β Backend API never became ready"
echo "π Services log:"
tail -n 30 services.log || true
exit 1
fi
sleep 3
done
# Wait for frontend
for i in {1..30}; do
if nc -z localhost 5173 2>/dev/null; then
echo "β
Frontend is ready on port 5173"
break
fi
if [ $i -eq 30 ]; then
echo "β Frontend never became ready on port 5173"
echo "π Services log:"
tail -n 30 services.log || true
exit 1
fi
sleep 2
done
echo "π Last few lines of services.log:"
tail -n 15 services.log || true
- name: Start ngrok tunnel (v4 CLI, verbose)
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
id: ngrok
env:
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
run: |
echo "π Establishing ngrok tunnel (v4, verbose mode enabled)..."
echo "π Using NGROK_AUTHTOKEN from environment (not stored on disk)."
echo "π Checking for active services..."
frontend_ready=false
backend_ready=false
# Check frontend (primary target for preview)
if nc -z localhost 5173; then
frontend_ready=true
target_port=5173
echo "β
Frontend detected on port 5173 (primary target)"
fi
# Check backend API
if nc -z localhost 3000; then
backend_ready=true
echo "β
Backend API detected on port 3000"
fi
# Fallback to other common ports if frontend not found
if [ "$frontend_ready" = false ]; then
for port in 3000 8080; do
echo " Testing fallback port $port..."
if nc -z localhost $port; then
target_port=$port
echo "β
Detected service on fallback port $target_port"
break
fi
done
fi
if [ -z "$target_port" ]; then
echo "β Could not detect active services on ports (5173, 3000, 8080)."
echo "πͺ΅ Showing last 20 lines of services.log:"
tail -n 20 services.log || true
exit 1
fi
echo "π― Using port $target_port for preview (frontend: $frontend_ready, backend: $backend_ready)"
echo "π Starting ngrok for port $target_port (v4 CLI)..."
ngrok version
# Start ngrok v4 CLI directly with environment auth token
nohup ngrok http $target_port --log=stdout > ngrok.log 2>&1 &
NGROK_PID=$!
echo "π§© ngrok process started with PID $NGROK_PID"
sleep 3
echo "β³ Waiting for ngrok API (http://127.0.0.1:4040) to become available..."
for i in {1..30}; do
url=$(curl -s --max-time 2 http://127.0.0.1:4040/api/tunnels | grep -o 'https://[^"]*' | head -n 1 || true)
if [ -n "$url" ]; then
echo "β
ngrok tunnel established successfully: $url"
echo "preview_url=$url" >> $GITHUB_OUTPUT
break
fi
echo " ...still waiting ($i/30)"
# Show partial ngrok log every 5 iterations for visibility
if (( i % 5 == 0 )); then
echo "π Partial ngrok log:"
tail -n 10 ngrok.log || true
fi
sleep 2
done
if [ -z "$url" ]; then
echo "β ngrok tunnel failed to start within timeout."
echo "π§Ύ Full ngrok log output for debugging:"
cat ngrok.log || true
echo "𧨠Killing ngrok process PID $NGROK_PID"
kill $NGROK_PID || true
exit 1
fi
echo "π ngrok setup complete and verified."
- name: Send Telegram message
if: ${{ steps.detect.outputs.frontend_found == 'true' && success() }}
env:
BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
REPO=${{ github.repository }}
PREVIEW_URL="${{ steps.ngrok.outputs.preview_url }}"
TIMEOUT_MINUTES=${{ env.PREVIEW_TIMEOUT_MINUTES }}
WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
if [ "${{ github.event_name }}" = "pull_request" ]; then
PR_NUMBER=${{ github.event.pull_request.number }}
PR_TITLE="${{ github.event.pull_request.title }}"
AUTHOR="${{ github.event.pull_request.user.login }}"
ESCAPED_TITLE=$(echo "$PR_TITLE" | sed 's/[-_*\[\]()~`>#+=|{}.!]/\\&/g')
ESCAPED_AUTHOR=$(echo "$AUTHOR" | sed 's/[-_*\[\]()~`>#+=|{}.!]/\\&/g')
ESCAPED_REPO=$(echo "$REPO" | sed 's/[-_*\[\]()~`>#+=|{}.!]/\\&/g')
MSG=$(printf '%s\n' \
"π *Preview ready for PR \\#$PR_NUMBER in $ESCAPED_REPO*" \
"" \
"*Title:* $ESCAPED_TITLE" \
"*Author:* $ESCAPED_AUTHOR" \
"" \
"*Live Preview:* [Open Preview]($PREVIEW_URL)" \
"*Workflow:* [View Run]($WORKFLOW_URL)" \
"β³ *This preview will expire in ${TIMEOUT_MINUTES} minutes\.*" \
"" \
"[View on GitHub](https://github.com/$REPO/pull/$PR_NUMBER)"
)
else
# Manual dispatch
BRANCH_NAME="${{ github.ref_name }}"
ACTOR="${{ github.actor }}"
ESCAPED_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[-_*\[\]()~`>#+=|{}.!]/\\&/g')
ESCAPED_ACTOR=$(echo "$ACTOR" | sed 's/[-_*\[\]()~`>#+=|{}.!]/\\&/g')
ESCAPED_REPO=$(echo "$REPO" | sed 's/[-_*\[\]()~`>#+=|{}.!]/\\&/g')
MSG=$(printf '%s\n' \
"π *Manual preview deployed for $ESCAPED_REPO*" \
"" \
"*Branch:* $ESCAPED_BRANCH" \
"*Triggered by:* $ESCAPED_ACTOR" \
"" \
"*Live Preview:* [Open Preview]($PREVIEW_URL)" \
"*Workflow:* [View Run]($WORKFLOW_URL)" \
"β³ *This preview will expire in ${TIMEOUT_MINUTES} minutes\.*"
)
fi
curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID" \
-d "parse_mode=MarkdownV2" \
--data-urlencode "text=$MSG"
- name: Keep ngrok alive
if: ${{ steps.detect.outputs.frontend_found == 'true' }}
run: |
echo "β³ Keeping ngrok tunnel alive for $PREVIEW_TIMEOUT_MINUTES minutes..."
sleep $(( PREVIEW_TIMEOUT_MINUTES * 60 ))