diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 3f04fa8a..4616b9d1 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -1,7 +1,6 @@ -name: Dev Release +name: CI and Dev Release on: - workflow_dispatch: push: branches: - main @@ -12,14 +11,23 @@ on: - 'packages/**/*.json' - '*.yaml' - '*.yml' + pull_request: + paths: + - 'packages/**/*.ts' + - 'packages/**/*.tsx' + - 'packages/**/*.lock' + - 'packages/**/*.json' + - '*.yaml' + - '*.yml' + workflow_dispatch: jobs: - test-and-release: - name: Test and Dev Release + # ------------------------------------------------------------------ # + # Unit Tests # + # ------------------------------------------------------------------ # + unit-tests: + name: Unit Tests runs-on: ubuntu-latest - permissions: - contents: write - packages: write steps: - name: Checkout repository @@ -46,9 +54,105 @@ jobs: run: bun test --verbose --workers=1 working-directory: ./packages/backend + # ------------------------------------------------------------------ # + # Binary Startup Test # + # ------------------------------------------------------------------ # + binary-startup: + name: Binary Startup Test + runs-on: ubuntu-latest + needs: unit-tests + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install Base dependencies + run: bun install --frozen-lockfile + + - name: Install Backend dependencies + run: bun install --frozen-lockfile + working-directory: ./packages/backend + + - name: Install Frontend dependencies + run: bun install --frozen-lockfile + working-directory: ./packages/frontend + + - name: Compile Linux binary + # compile:linux also runs build:frontend first + run: bun run compile:linux + + - name: Run binary startup test + run: bash scripts/test-startup-binary.sh + + # ------------------------------------------------------------------ # + # Docker Startup Test # + # ------------------------------------------------------------------ # + docker-startup: + name: Docker Startup Test + runs-on: ubuntu-latest + needs: unit-tests + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + load: true + tags: plexus-test:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Docker startup test + run: bash scripts/test-startup-docker.sh plexus-test:latest + + # ------------------------------------------------------------------ # + # Dev Release (main branch / workflow_dispatch only) # + # ------------------------------------------------------------------ # + dev-release: + name: Dev Release + runs-on: ubuntu-latest + needs: [binary-startup, docker-startup] + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' + permissions: + contents: write + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install Base dependencies + run: bun install --frozen-lockfile + working-directory: . + + - name: Install Backend dependencies + run: bun install --frozen-lockfile + working-directory: ./packages/backend + + - name: Install Frontend dependencies + run: bun install --frozen-lockfile + working-directory: ./packages/frontend + - name: Compile Binaries # compiling each platform separately - # Note: Each compile script in package.json runs build:frontend. + # Note: Each compile script in package.json runs build:frontend. # This is acceptable for correctness to ensure each binary is built with the latest assets. env: APP_VERSION: dev-${{ github.sha }} @@ -94,5 +198,5 @@ jobs: tag_name: dev-${{ github.sha }} body: | Development pre-release from commit ${{ github.sha }} - + Built from: `${{ github.ref }}` at `${{ github.sha }}` diff --git a/scripts/test-startup-binary.sh b/scripts/test-startup-binary.sh new file mode 100755 index 00000000..c2e086f7 --- /dev/null +++ b/scripts/test-startup-binary.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# test-startup-binary.sh +# Smoke-test the compiled Linux binary: verify it starts, serves /health, and loads /ui/. +# +# Usage: bash scripts/test-startup-binary.sh +# Expected working directory: repository root (where plexus-linux lives after compile:linux) +set -euo pipefail + +ADMIN_KEY="test-startup-key-ci" +PORT=14000 +TIMEOUT=60 +BINARY_WORKDIR="packages/backend" +BINARY_NAME="plexus-linux" +LOG_FILE="/tmp/plexus-binary-startup.log" + +echo "=== Binary Startup Test ===" + +# ------------------------------------------------------------------ +# Cleanup: kill background process and remove temp binary copy +# ------------------------------------------------------------------ +cleanup() { + if [[ -n "${PID:-}" ]]; then + echo "Stopping binary (PID=$PID)..." + kill "$PID" 2>/dev/null || true + wait "$PID" 2>/dev/null || true + fi + rm -f "${BINARY_WORKDIR}/${BINARY_NAME}" +} +trap cleanup EXIT + +# ------------------------------------------------------------------ +# Stage binary in the backend package directory so that the static +# file path ../frontend/dist resolves to packages/frontend/dist. +# ------------------------------------------------------------------ +if [[ ! -f "./${BINARY_NAME}" ]]; then + echo "ERROR: ./${BINARY_NAME} not found. Run 'bun run compile:linux' first." + exit 1 +fi + +cp "./${BINARY_NAME}" "${BINARY_WORKDIR}/${BINARY_NAME}" +chmod +x "${BINARY_WORKDIR}/${BINARY_NAME}" + +# ------------------------------------------------------------------ +# Start the binary +# ------------------------------------------------------------------ +echo "Starting binary from ${BINARY_WORKDIR}/..." +pushd "${BINARY_WORKDIR}" >/dev/null +DATABASE_URL="sqlite://:memory:" \ + ADMIN_KEY="${ADMIN_KEY}" \ + PORT="${PORT}" \ + LOG_LEVEL="info" \ + "./${BINARY_NAME}" > "${LOG_FILE}" 2>&1 & +PID=$! +popd >/dev/null +echo "Binary PID: ${PID}" + +# ------------------------------------------------------------------ +# Wait for the "Server starting on port" log line +# ------------------------------------------------------------------ +echo "Waiting for server to start (timeout: ${TIMEOUT}s)..." +STARTED=0 +for i in $(seq 1 "${TIMEOUT}"); do + if grep -q "Server starting on port" "${LOG_FILE}" 2>/dev/null; then + echo "Server ready (detected after ~${i}s)" + STARTED=1 + break + fi + if ! kill -0 "${PID}" 2>/dev/null; then + echo "ERROR: Binary exited prematurely after ${i}s" + echo "=== Log output ===" + cat "${LOG_FILE}" + exit 1 + fi + sleep 1 +done + +if [[ "${STARTED}" -eq 0 ]]; then + echo "ERROR: Server did not log readiness within ${TIMEOUT}s" + echo "=== Log output ===" + cat "${LOG_FILE}" + exit 1 +fi + +# Brief pause to ensure the TCP port is fully accepting connections +sleep 1 + +# ------------------------------------------------------------------ +# Test /health +# ------------------------------------------------------------------ +echo "Testing GET /health ..." +HEALTH_RESPONSE=$(curl -sf --max-time 10 "http://localhost:${PORT}/health" || echo "CURL_FAILED") +if [[ "${HEALTH_RESPONSE}" != "OK" ]]; then + echo "ERROR: /health returned unexpected response: '${HEALTH_RESPONSE}'" + echo "=== Log output ===" + cat "${LOG_FILE}" + exit 1 +fi +echo " /health -> OK" + +# ------------------------------------------------------------------ +# Test /ui/ returns HTTP 200 with HTML content +# ------------------------------------------------------------------ +echo "Testing GET /ui/ ..." +HTTP_CODE=$(curl -s --max-time 10 -o /tmp/plexus-binary-ui.html -w "%{http_code}" \ + "http://localhost:${PORT}/ui/") +if [[ "${HTTP_CODE}" != "200" ]]; then + echo "ERROR: /ui/ returned HTTP ${HTTP_CODE}" + echo "=== Response body ===" + cat /tmp/plexus-binary-ui.html 2>/dev/null || true + echo "=== Log output ===" + cat "${LOG_FILE}" + exit 1 +fi +if ! grep -qi "/dev/null; then + echo "ERROR: /ui/ response does not appear to be HTML" + echo "=== Response body (first 512 bytes) ===" + head -c 512 /tmp/plexus-binary-ui.html + echo "=== Log output ===" + cat "${LOG_FILE}" + exit 1 +fi +echo " /ui/ -> HTTP ${HTTP_CODE} (HTML confirmed)" + +echo "" +echo "Binary startup test PASSED" diff --git a/scripts/test-startup-docker.sh b/scripts/test-startup-docker.sh new file mode 100755 index 00000000..30f822c1 --- /dev/null +++ b/scripts/test-startup-docker.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# test-startup-docker.sh +# Smoke-test the Docker image: verify it starts, serves /health, and loads /ui/. +# +# Usage: bash scripts/test-startup-docker.sh [image-tag] +# Expected working directory: repository root +# Default image tag: plexus-test:latest +set -euo pipefail + +IMAGE="${1:-plexus-test:latest}" +ADMIN_KEY="test-startup-key-ci" +HOST_PORT=14001 +CONTAINER_PORT=4000 +TIMEOUT=60 +CONTAINER_NAME="plexus-startup-test-$$" + +echo "=== Docker Startup Test (${IMAGE}) ===" + +# ------------------------------------------------------------------ +# Cleanup: always stop and remove the test container +# ------------------------------------------------------------------ +cleanup() { + if docker inspect "${CONTAINER_NAME}" &>/dev/null; then + echo "Stopping container ${CONTAINER_NAME}..." + docker stop "${CONTAINER_NAME}" 2>/dev/null || true + docker rm "${CONTAINER_NAME}" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# ------------------------------------------------------------------ +# Start the container with an in-memory SQLite database so no +# persistent volume is required. +# ------------------------------------------------------------------ +echo "Starting container..." +docker run -d \ + --name "${CONTAINER_NAME}" \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + -e "ADMIN_KEY=${ADMIN_KEY}" \ + -e "DATABASE_URL=sqlite://:memory:" \ + -e "LOG_LEVEL=info" \ + "${IMAGE}" + +echo "Container started: ${CONTAINER_NAME}" + +# ------------------------------------------------------------------ +# Wait for the "Server starting on port" log line +# ------------------------------------------------------------------ +echo "Waiting for server to start (timeout: ${TIMEOUT}s)..." +STARTED=0 +for i in $(seq 1 "${TIMEOUT}"); do + if docker logs "${CONTAINER_NAME}" 2>&1 | grep -q "Server starting on port"; then + echo "Server ready (detected after ~${i}s)" + STARTED=1 + break + fi + # Check if the container is still running + STATUS=$(docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo "missing") + if [[ "${STATUS}" != "running" ]]; then + echo "ERROR: Container stopped unexpectedly after ${i}s (status=${STATUS})" + echo "=== Container logs ===" + docker logs "${CONTAINER_NAME}" 2>&1 || true + exit 1 + fi + sleep 1 +done + +if [[ "${STARTED}" -eq 0 ]]; then + echo "ERROR: Server did not log readiness within ${TIMEOUT}s" + echo "=== Container logs ===" + docker logs "${CONTAINER_NAME}" 2>&1 || true + exit 1 +fi + +# Brief pause to ensure the TCP port is fully accepting connections +sleep 1 + +# ------------------------------------------------------------------ +# Test /health +# ------------------------------------------------------------------ +echo "Testing GET /health ..." +HEALTH_RESPONSE=$(curl -sf --max-time 10 "http://localhost:${HOST_PORT}/health" || echo "CURL_FAILED") +if [[ "${HEALTH_RESPONSE}" != "OK" ]]; then + echo "ERROR: /health returned unexpected response: '${HEALTH_RESPONSE}'" + echo "=== Container logs ===" + docker logs "${CONTAINER_NAME}" 2>&1 || true + exit 1 +fi +echo " /health -> OK" + +# ------------------------------------------------------------------ +# Test /ui/ returns HTTP 200 with HTML content +# ------------------------------------------------------------------ +echo "Testing GET /ui/ ..." +HTTP_CODE=$(curl -s --max-time 10 -o /tmp/plexus-docker-ui.html -w "%{http_code}" \ + "http://localhost:${HOST_PORT}/ui/") +if [[ "${HTTP_CODE}" != "200" ]]; then + echo "ERROR: /ui/ returned HTTP ${HTTP_CODE}" + echo "=== Response body ===" + cat /tmp/plexus-docker-ui.html 2>/dev/null || true + echo "=== Container logs ===" + docker logs "${CONTAINER_NAME}" 2>&1 || true + exit 1 +fi +if ! grep -qi "/dev/null; then + echo "ERROR: /ui/ response does not appear to be HTML" + echo "=== Response body (first 512 bytes) ===" + head -c 512 /tmp/plexus-docker-ui.html + echo "=== Container logs ===" + docker logs "${CONTAINER_NAME}" 2>&1 || true + exit 1 +fi +echo " /ui/ -> HTTP ${HTTP_CODE} (HTML confirmed)" + +echo "" +echo "Docker startup test PASSED"