mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 21:45:40 +00:00
- Merge latest master (concurrent E2E + coverage, Playwright install split)
- Fix 'Packets clicking row shows detail pane' and 'Packet detail pane
closes on ✕ click' E2E tests: wait for networkidle before interacting
with table rows, and use waitForResponse to ensure the /packets/{hash}
API call completes before asserting panel state. The row click triggers
an async fetch that was silently failing in CI due to race conditions.
460 lines
20 KiB
YAML
460 lines
20 KiB
YAML
name: Deploy
|
|
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
paths-ignore:
|
|
- '**.md'
|
|
- 'LICENSE'
|
|
- '.gitignore'
|
|
- 'docs/**'
|
|
pull_request:
|
|
branches: [master]
|
|
|
|
concurrency:
|
|
group: deploy-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
|
|
# Pipeline:
|
|
# node-test (frontend tests) ──┐
|
|
# go-test ├──→ build → deploy → publish
|
|
# └─ (both wait)
|
|
#
|
|
# Proto validation flow:
|
|
# 1. go-test job: verify .proto files compile (syntax check)
|
|
# 2. deploy job: capture fresh fixtures from prod, validate protos match actual API responses
|
|
|
|
jobs:
|
|
# ───────────────────────────────────────────────────────────────
|
|
# 1. Go Build & Test — compiles + tests Go modules, coverage badges
|
|
# ───────────────────────────────────────────────────────────────
|
|
go-test:
|
|
name: "✅ Go Build & Test"
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Skip if docs-only change
|
|
id: docs-check
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
|
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
|
if [ "$NON_DOCS" -eq 0 ]; then
|
|
echo "docs_only=true" >> $GITHUB_OUTPUT
|
|
echo "📄 Docs-only PR — skipping heavy CI"
|
|
fi
|
|
fi
|
|
|
|
- name: Set up Go 1.22
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: '1.22'
|
|
cache-dependency-path: |
|
|
cmd/server/go.sum
|
|
cmd/ingestor/go.sum
|
|
|
|
- name: Build and test Go server (with coverage)
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
run: |
|
|
set -e -o pipefail
|
|
cd cmd/server
|
|
go build .
|
|
go test -coverprofile=server-coverage.out ./... 2>&1 | tee server-test.log
|
|
echo "--- Go Server Coverage ---"
|
|
go tool cover -func=server-coverage.out | tail -1
|
|
|
|
- name: Build and test Go ingestor (with coverage)
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
run: |
|
|
set -e -o pipefail
|
|
cd cmd/ingestor
|
|
go build .
|
|
go test -coverprofile=ingestor-coverage.out ./... 2>&1 | tee ingestor-test.log
|
|
echo "--- Go Ingestor Coverage ---"
|
|
go tool cover -func=ingestor-coverage.out | tail -1
|
|
|
|
- name: Verify proto syntax (all .proto files compile)
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
run: |
|
|
set -e
|
|
echo "Installing protoc..."
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y protobuf-compiler
|
|
|
|
echo "Checking proto syntax..."
|
|
for proto in proto/*.proto; do
|
|
echo " ✓ $(basename "$proto")"
|
|
protoc --proto_path=proto --descriptor_set_out=/dev/null "$proto"
|
|
done
|
|
echo "✅ All .proto files are syntactically valid"
|
|
|
|
- name: Generate Go coverage badges
|
|
if: always() && steps.docs-check.outputs.docs_only != 'true'
|
|
run: |
|
|
mkdir -p .badges
|
|
|
|
# Parse server coverage
|
|
SERVER_COV="0"
|
|
if [ -f cmd/server/server-coverage.out ]; then
|
|
SERVER_COV=$(cd cmd/server && go tool cover -func=server-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
|
|
fi
|
|
SERVER_COLOR="red"
|
|
if [ "$(echo "$SERVER_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then
|
|
SERVER_COLOR="green"
|
|
elif [ "$(echo "$SERVER_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then
|
|
SERVER_COLOR="yellow"
|
|
fi
|
|
echo "{\"schemaVersion\":1,\"label\":\"go server coverage\",\"message\":\"${SERVER_COV}%\",\"color\":\"${SERVER_COLOR}\"}" > .badges/go-server-coverage.json
|
|
echo "Go server coverage: ${SERVER_COV}% (${SERVER_COLOR})"
|
|
|
|
# Parse ingestor coverage
|
|
INGESTOR_COV="0"
|
|
if [ -f cmd/ingestor/ingestor-coverage.out ]; then
|
|
INGESTOR_COV=$(cd cmd/ingestor && go tool cover -func=ingestor-coverage.out | tail -1 | grep -oP '[\d.]+(?=%)')
|
|
fi
|
|
INGESTOR_COLOR="red"
|
|
if [ "$(echo "$INGESTOR_COV >= 80" | bc -l 2>/dev/null)" = "1" ]; then
|
|
INGESTOR_COLOR="green"
|
|
elif [ "$(echo "$INGESTOR_COV >= 60" | bc -l 2>/dev/null)" = "1" ]; then
|
|
INGESTOR_COLOR="yellow"
|
|
fi
|
|
echo "{\"schemaVersion\":1,\"label\":\"go ingestor coverage\",\"message\":\"${INGESTOR_COV}%\",\"color\":\"${INGESTOR_COLOR}\"}" > .badges/go-ingestor-coverage.json
|
|
echo "Go ingestor coverage: ${INGESTOR_COV}% (${INGESTOR_COLOR})"
|
|
|
|
echo "## Go Coverage" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Module | Coverage |" >> $GITHUB_STEP_SUMMARY
|
|
echo "|--------|----------|" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Server | ${SERVER_COV}% |" >> $GITHUB_STEP_SUMMARY
|
|
echo "| Ingestor | ${INGESTOR_COV}% |" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Cancel workflow on failure
|
|
if: failure()
|
|
run: |
|
|
curl -s -X POST \
|
|
-H "Authorization: Bearer ${{ github.token }}" \
|
|
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
|
|
|
|
- name: Upload Go coverage badges
|
|
if: always()
|
|
uses: actions/upload-artifact@v5
|
|
with:
|
|
name: go-badges
|
|
path: .badges/go-*.json
|
|
retention-days: 1
|
|
if-no-files-found: ignore
|
|
|
|
# ───────────────────────────────────────────────────────────────
|
|
# 2. Node.js Tests — backend unit tests + Playwright E2E, coverage
|
|
# ───────────────────────────────────────────────────────────────
|
|
node-test:
|
|
name: "🧪 Node.js Tests"
|
|
runs-on: [self-hosted, Linux]
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Skip if docs-only change
|
|
id: docs-check
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
|
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
|
if [ "$NON_DOCS" -eq 0 ]; then
|
|
echo "docs_only=true" >> $GITHUB_OUTPUT
|
|
echo "📄 Docs-only PR — skipping heavy CI"
|
|
fi
|
|
fi
|
|
|
|
- name: Set up Node.js 22
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
uses: actions/setup-node@v5
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Install npm dependencies
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
run: npm ci --production=false
|
|
|
|
- name: Detect changed files
|
|
if: steps.docs-check.outputs.docs_only != 'true'
|
|
id: changes
|
|
run: |
|
|
BACKEND=$(git diff --name-only HEAD~1 | grep -cE '^(server|db|decoder|packet-store|server-helpers|iata-coords)\.js$' || true)
|
|
FRONTEND=$(git diff --name-only HEAD~1 | grep -cE '^public/' || true)
|
|
TESTS=$(git diff --name-only HEAD~1 | grep -cE '^test-|^tools/' || true)
|
|
CI=$(git diff --name-only HEAD~1 | grep -cE '\.github/|package\.json|test-all\.sh|scripts/' || true)
|
|
# If CI/test infra changed, run everything
|
|
if [ "$CI" -gt 0 ]; then BACKEND=1; FRONTEND=1; fi
|
|
# If test files changed, run everything
|
|
if [ "$TESTS" -gt 0 ]; then BACKEND=1; FRONTEND=1; fi
|
|
echo "backend=$([[ $BACKEND -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
|
|
echo "frontend=$([[ $FRONTEND -gt 0 ]] && echo true || echo false)" >> $GITHUB_OUTPUT
|
|
echo "Changes: backend=$BACKEND frontend=$FRONTEND tests=$TESTS ci=$CI"
|
|
|
|
- name: Run backend tests with coverage
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'true'
|
|
run: |
|
|
npx c8 --reporter=text-summary --reporter=text sh test-all.sh 2>&1 | tee test-output.txt
|
|
|
|
TOTAL_PASS=$(grep -oP '\d+(?= passed)' test-output.txt | awk '{s+=$1} END {print s}')
|
|
TOTAL_FAIL=$(grep -oP '\d+(?= failed)' test-output.txt | awk '{s+=$1} END {print s}')
|
|
BE_COVERAGE=$(grep 'Statements' test-output.txt | tail -1 | grep -oP '[\d.]+(?=%)')
|
|
|
|
mkdir -p .badges
|
|
BE_COLOR="red"
|
|
[ "$(echo "$BE_COVERAGE > 60" | bc -l 2>/dev/null)" = "1" ] && BE_COLOR="yellow"
|
|
[ "$(echo "$BE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && BE_COLOR="brightgreen"
|
|
echo "{\"schemaVersion\":1,\"label\":\"backend tests\",\"message\":\"${TOTAL_PASS} passed\",\"color\":\"brightgreen\"}" > .badges/backend-tests.json
|
|
echo "{\"schemaVersion\":1,\"label\":\"backend coverage\",\"message\":\"${BE_COVERAGE}%\",\"color\":\"${BE_COLOR}\"}" > .badges/backend-coverage.json
|
|
|
|
echo "## Backend: ${TOTAL_PASS} tests, ${BE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
|
|
|
- name: Run backend tests (quick, no coverage)
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'false'
|
|
run: npm run test:unit
|
|
|
|
- name: Install Playwright browser
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
|
run: |
|
|
# Install chromium (skips download if already cached on self-hosted runner)
|
|
npx playwright install chromium 2>/dev/null || true
|
|
# Install system deps only if missing (apt-get is slow)
|
|
npx playwright install-deps chromium 2>/dev/null || true
|
|
|
|
- name: Instrument frontend JS for coverage
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
|
run: sh scripts/instrument-frontend.sh
|
|
|
|
- name: Start instrumented test server on port 13581
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
|
run: |
|
|
# Kill any stale server on 13581
|
|
fuser -k 13581/tcp 2>/dev/null || true
|
|
sleep 2
|
|
COVERAGE=1 PORT=13581 node server.js &
|
|
echo $! > .server.pid
|
|
echo "Server PID: $(cat .server.pid)"
|
|
# Health-check poll loop (up to 30s)
|
|
for i in $(seq 1 30); do
|
|
if curl -sf http://localhost:13581/api/stats > /dev/null 2>&1; then
|
|
echo "Server ready after ${i}s"
|
|
break
|
|
fi
|
|
if [ "$i" -eq 30 ]; then
|
|
echo "Server failed to start within 30s"
|
|
echo "Last few lines from server logs:"
|
|
ps aux | grep "PORT=13581" || echo "No server process found"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
- name: Run Playwright E2E + coverage collection concurrently
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
|
run: |
|
|
# Run E2E tests and coverage collection in parallel — both use the same server
|
|
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt &
|
|
E2E_PID=$!
|
|
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt &
|
|
COV_PID=$!
|
|
|
|
# Wait for both — E2E must pass, coverage is best-effort
|
|
E2E_EXIT=0
|
|
wait $E2E_PID || E2E_EXIT=$?
|
|
wait $COV_PID || true
|
|
|
|
# Fail if E2E failed
|
|
[ $E2E_EXIT -ne 0 ] && exit $E2E_EXIT
|
|
true
|
|
|
|
- name: Generate frontend coverage badges
|
|
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
|
run: |
|
|
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1)
|
|
|
|
mkdir -p .badges
|
|
# Merge E2E + coverage collector data if both exist
|
|
if [ -f .nyc_output/frontend-coverage.json ] || [ -f .nyc_output/e2e-coverage.json ]; then
|
|
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
|
|
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
|
|
FE_COVERAGE=${FE_COVERAGE:-0}
|
|
FE_COLOR="red"
|
|
[ "$(echo "$FE_COVERAGE > 50" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="yellow"
|
|
[ "$(echo "$FE_COVERAGE > 80" | bc -l 2>/dev/null)" = "1" ] && FE_COLOR="brightgreen"
|
|
echo "{\"schemaVersion\":1,\"label\":\"frontend coverage\",\"message\":\"${FE_COVERAGE}%\",\"color\":\"${FE_COLOR}\"}" > .badges/frontend-coverage.json
|
|
echo "## Frontend: ${FE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
|
fi
|
|
echo "{\"schemaVersion\":1,\"label\":\"frontend tests\",\"message\":\"${E2E_PASS:-0} E2E passed\",\"color\":\"brightgreen\"}" > .badges/frontend-tests.json
|
|
|
|
- name: Stop test server
|
|
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
|
run: |
|
|
if [ -f .server.pid ]; then
|
|
kill $(cat .server.pid) 2>/dev/null || true
|
|
rm -f .server.pid
|
|
echo "Server stopped"
|
|
fi
|
|
|
|
- name: Run frontend E2E (quick, no coverage)
|
|
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'false'
|
|
run: |
|
|
fuser -k 13581/tcp 2>/dev/null || true
|
|
PORT=13581 node server.js &
|
|
SERVER_PID=$!
|
|
# Wait for server to be ready (up to 15s)
|
|
for i in $(seq 1 15); do
|
|
curl -sf http://localhost:13581/api/stats > /dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
|
|
kill $SERVER_PID 2>/dev/null || true
|
|
|
|
- name: Cancel workflow on failure
|
|
if: failure()
|
|
run: |
|
|
curl -s -X POST \
|
|
-H "Authorization: Bearer ${{ github.token }}" \
|
|
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/cancel"
|
|
|
|
- name: Upload Node.js test badges
|
|
if: always()
|
|
uses: actions/upload-artifact@v5
|
|
with:
|
|
name: node-badges
|
|
path: .badges/
|
|
retention-days: 1
|
|
if-no-files-found: ignore
|
|
|
|
# ───────────────────────────────────────────────────────────────
|
|
# 3. Build Docker Image
|
|
# ───────────────────────────────────────────────────────────────
|
|
build:
|
|
name: "🏗️ Build Docker Image"
|
|
if: github.event_name == 'push'
|
|
needs: [go-test, node-test]
|
|
runs-on: [self-hosted, Linux]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Set up Node.js 22
|
|
uses: actions/setup-node@v5
|
|
with:
|
|
node-version: '22'
|
|
|
|
- name: Build Go Docker image
|
|
run: |
|
|
echo "${GITHUB_SHA::7}" > .git-commit
|
|
APP_VERSION=$(node -p "require('./package.json').version") \
|
|
GIT_COMMIT="${GITHUB_SHA::7}" \
|
|
docker compose --profile staging-go build staging-go
|
|
echo "Built Go staging image"
|
|
|
|
# ───────────────────────────────────────────────────────────────
|
|
# 4. Deploy Staging — start on port 82, healthcheck, smoke test
|
|
# ───────────────────────────────────────────────────────────────
|
|
deploy:
|
|
name: "🚀 Deploy Staging"
|
|
if: github.event_name == 'push'
|
|
needs: [build]
|
|
runs-on: [self-hosted, Linux]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Start staging on port 82
|
|
run: |
|
|
# Force remove stale containers
|
|
docker rm -f corescope-staging-go 2>/dev/null || true
|
|
# Clean up stale ports
|
|
fuser -k 82/tcp 2>/dev/null || true
|
|
docker compose --profile staging-go up -d staging-go
|
|
|
|
- name: Healthcheck staging container
|
|
run: |
|
|
for i in $(seq 1 120); do
|
|
HEALTH=$(docker inspect corescope-staging-go --format '{{.State.Health.Status}}' 2>/dev/null || echo "starting")
|
|
if [ "$HEALTH" = "healthy" ]; then
|
|
echo "Staging healthy after ${i}s"
|
|
break
|
|
fi
|
|
if [ "$i" -eq 120 ]; then
|
|
echo "Staging failed health check after 120s"
|
|
docker logs corescope-staging-go --tail 50
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
- name: Smoke test staging API
|
|
run: |
|
|
if curl -sf http://localhost:82/api/stats | grep -q engine; then
|
|
echo "Staging verified — engine field present ✅"
|
|
else
|
|
echo "Staging /api/stats did not return engine field"
|
|
exit 1
|
|
fi
|
|
|
|
# ───────────────────────────────────────────────────────────────
|
|
# 5. Publish Badges & Summary
|
|
# ───────────────────────────────────────────────────────────────
|
|
publish:
|
|
name: "📝 Publish Badges & Summary"
|
|
if: github.event_name == 'push'
|
|
needs: [deploy]
|
|
runs-on: [self-hosted, Linux]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Download Go coverage badges
|
|
continue-on-error: true
|
|
uses: actions/download-artifact@v5
|
|
with:
|
|
name: go-badges
|
|
path: .badges/
|
|
|
|
- name: Download Node.js test badges
|
|
continue-on-error: true
|
|
uses: actions/download-artifact@v5
|
|
with:
|
|
name: node-badges
|
|
path: .badges/
|
|
|
|
- name: Publish coverage badges to repo
|
|
continue-on-error: true
|
|
run: |
|
|
git config user.name "github-actions"
|
|
git config user.email "actions@github.com"
|
|
git remote set-url origin https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git
|
|
git add .badges/ -f
|
|
git diff --cached --quiet || (git commit -m "ci: update test badges [skip ci]" && git push) || echo "Badge push failed"
|
|
|
|
- name: Post deployment summary
|
|
run: |
|
|
echo "## Staging Deployed ✓" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Commit:** \`$(git rev-parse --short HEAD)\` — $(git log -1 --format=%s)" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Staging:** http://<VM_HOST>:82" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "To promote to production:" >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
|
echo "ssh deploy@\$VM_HOST" >> $GITHUB_STEP_SUMMARY
|
|
echo "cd /opt/corescope-deploy" >> $GITHUB_STEP_SUMMARY
|
|
echo "./manage.sh promote" >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|