306 lines
12 KiB
Bash
Executable File
306 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# test-on-daytona.sh — one-command Daytona Electron test sandbox
|
|
#
|
|
# Usage:
|
|
# bash .devcontainer/test-on-daytona.sh [branch-or-commit]
|
|
# bash .devcontainer/test-on-daytona.sh [branch-or-commit] --force-install
|
|
# bash .devcontainer/test-on-daytona.sh [branch-or-commit] --artifacts-volume
|
|
# bash .devcontainer/test-on-daytona.sh [branch-or-commit] --record-video
|
|
# bash .devcontainer/setup-daytona-secrets-volume.sh [.newtoken]
|
|
#
|
|
# Creates a Daytona sandbox from the reusable VNC snapshot, checks out the given
|
|
# ref, installs dependencies with a workspace-local pnpm store when needed,
|
|
# starts `pnpm dev:sandbox`, and prints CDP/noVNC URLs. Optionally mounts an
|
|
# artifacts volume and records the Electron display.
|
|
|
|
REF=""
|
|
FORCE_INSTALL=0
|
|
DEN_BASE_URL=""
|
|
DEN_API_BASE_URL=""
|
|
DEN_REQUIRE_SIGNIN=0
|
|
ARTIFACTS_ENABLED=0
|
|
RECORD_VIDEO=0
|
|
RECORDING_NAME=""
|
|
RECORDING_FPS="15"
|
|
RECORDING_SIZE="1920x1080"
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--force-install)
|
|
FORCE_INSTALL=1
|
|
;;
|
|
--snapshot)
|
|
shift
|
|
DAYTONA_EVAL_SNAPSHOT="${1:?missing snapshot name}"
|
|
;;
|
|
--den-base-url)
|
|
shift
|
|
DEN_BASE_URL="${1:?missing Den base URL}"
|
|
;;
|
|
--den-api-base-url)
|
|
shift
|
|
DEN_API_BASE_URL="${1:?missing Den API base URL}"
|
|
;;
|
|
--require-signin)
|
|
DEN_REQUIRE_SIGNIN=1
|
|
;;
|
|
--artifacts-volume)
|
|
ARTIFACTS_ENABLED=1
|
|
;;
|
|
--record-video)
|
|
ARTIFACTS_ENABLED=1
|
|
RECORD_VIDEO=1
|
|
;;
|
|
--recording-name)
|
|
shift
|
|
RECORDING_NAME="${1:?missing recording name}"
|
|
;;
|
|
--recording-fps)
|
|
shift
|
|
RECORDING_FPS="${1:?missing recording fps}"
|
|
;;
|
|
--recording-size)
|
|
shift
|
|
RECORDING_SIZE="${1:?missing recording size}"
|
|
;;
|
|
--help|-h)
|
|
sed -n '1,16p' "$0"
|
|
exit 0
|
|
;;
|
|
--*)
|
|
echo "Unknown option: $1" >&2
|
|
exit 1
|
|
;;
|
|
*)
|
|
if [ -n "$REF" ]; then
|
|
echo "Unexpected extra argument: $1" >&2
|
|
exit 1
|
|
fi
|
|
REF="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ -z "$REF" ]; then
|
|
REF="$(git branch --show-current 2>/dev/null || true)"
|
|
REF="${REF:-$(git rev-parse HEAD)}"
|
|
fi
|
|
|
|
SANDBOX="openwork-test-$(date +%Y%m%d-%H%M%S)"
|
|
CDP_PORT=9825
|
|
NOVNC_PORT=6080
|
|
ARTIFACTS_PORT=8090
|
|
MAX_WAIT=90
|
|
DAYTONA_EVAL_SNAPSHOT="${DAYTONA_EVAL_SNAPSHOT:-openwork-eval-vnc}"
|
|
DAYTONA_SECRETS_VOLUME="${DAYTONA_SECRETS_VOLUME:-openwork-eval-secrets}"
|
|
DAYTONA_SECRETS_MOUNT="${DAYTONA_SECRETS_MOUNT:-/daytona-secrets}"
|
|
DAYTONA_SECRETS_ENV="${DAYTONA_SECRETS_ENV:-${DAYTONA_SECRETS_MOUNT}}"
|
|
DAYTONA_ARTIFACTS_VOLUME="${DAYTONA_ARTIFACTS_VOLUME:-openwork-eval-artifacts}"
|
|
DAYTONA_ARTIFACTS_MOUNT="${DAYTONA_ARTIFACTS_MOUNT:-/daytona-artifacts}"
|
|
DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS="${DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS:---disable-gpu --disable-dev-shm-usage --enable-unsafe-swiftshader}"
|
|
OPENWORK_ELECTRON_FAKE_MEDIA="${OPENWORK_ELECTRON_FAKE_MEDIA:-0}"
|
|
|
|
if [ -z "$RECORDING_NAME" ]; then
|
|
RECORDING_NAME="$SANDBOX"
|
|
fi
|
|
if [[ ! "$RECORDING_NAME" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
|
echo "ERROR: recording name may only contain letters, numbers, dots, underscores, and dashes" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "$RECORDING_FPS" =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: recording fps must be a positive integer" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "$RECORDING_SIZE" =~ ^[0-9]+x[0-9]+$ ]]; then
|
|
echo "ERROR: recording size must use WxH format, for example 1920x1080" >&2
|
|
exit 1
|
|
fi
|
|
|
|
volume_field() {
|
|
local name="$1"
|
|
local field="$2"
|
|
daytona volume list -f json | node -e 'const name = process.argv[1]; const field = process.argv[2]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const volume = JSON.parse(input).find((item) => item.name === name); if (!volume) process.exit(1); process.stdout.write(String(volume[field] ?? "")); });' "$name" "$field"
|
|
}
|
|
|
|
wait_for_volume_ready() {
|
|
local name="$1"
|
|
local state=""
|
|
|
|
for _ in $(seq 1 60); do
|
|
state="$(volume_field "$name" state 2>/dev/null || true)"
|
|
if [ "$state" = "ready" ]; then
|
|
return 0
|
|
fi
|
|
if [ "$state" = "error" ]; then
|
|
echo "ERROR: volume '$name' is in error state" >&2
|
|
exit 1
|
|
fi
|
|
sleep 5
|
|
done
|
|
|
|
echo "ERROR: volume '$name' did not become ready (last state: ${state:-missing})" >&2
|
|
exit 1
|
|
}
|
|
|
|
snapshot_id() {
|
|
daytona snapshot list -f json | node -e 'const name = process.argv[1]; let input = ""; process.stdin.on("data", (chunk) => input += chunk); process.stdin.on("end", () => { const snapshot = JSON.parse(input).find((item) => item.name === name); if (snapshot) process.stdout.write(snapshot.id || snapshot.name); });' "$1"
|
|
}
|
|
|
|
echo "==> Creating sandbox: $SANDBOX"
|
|
echo " Ref: $REF"
|
|
echo ""
|
|
|
|
echo "==> Ensuring reusable secrets volume: $DAYTONA_SECRETS_VOLUME"
|
|
volume_create_output="$(daytona volume create "$DAYTONA_SECRETS_VOLUME" --size 1 2>&1 || true)"
|
|
if [ -n "$volume_create_output" ] && ! printf '%s' "$volume_create_output" | grep -qi "already exists"; then
|
|
printf '%s\n' "$volume_create_output"
|
|
fi
|
|
wait_for_volume_ready "$DAYTONA_SECRETS_VOLUME"
|
|
DAYTONA_SECRETS_VOLUME_ID="$(volume_field "$DAYTONA_SECRETS_VOLUME" id)"
|
|
|
|
DAYTONA_ARTIFACTS_VOLUME_ID=""
|
|
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
|
|
echo ""
|
|
echo "==> Ensuring reusable artifacts volume: $DAYTONA_ARTIFACTS_VOLUME"
|
|
volume_create_output="$(daytona volume create "$DAYTONA_ARTIFACTS_VOLUME" --size 5 2>&1 || true)"
|
|
if [ -n "$volume_create_output" ] && ! printf '%s' "$volume_create_output" | grep -qi "already exists"; then
|
|
printf '%s\n' "$volume_create_output"
|
|
fi
|
|
wait_for_volume_ready "$DAYTONA_ARTIFACTS_VOLUME"
|
|
DAYTONA_ARTIFACTS_VOLUME_ID="$(volume_field "$DAYTONA_ARTIFACTS_VOLUME" id)"
|
|
fi
|
|
|
|
SNAPSHOT_ID="$(snapshot_id "$DAYTONA_EVAL_SNAPSHOT")"
|
|
if [ -z "$SNAPSHOT_ID" ]; then
|
|
echo "ERROR: Daytona snapshot '$DAYTONA_EVAL_SNAPSHOT' not found." >&2
|
|
echo "Create it with: bash .devcontainer/create-daytona-openwork-snapshot.sh" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "==> Using Daytona snapshot: $DAYTONA_EVAL_SNAPSHOT"
|
|
DAYTONA_CREATE_ARGS=(
|
|
--name "$SANDBOX"
|
|
--snapshot "$SNAPSHOT_ID"
|
|
--volume "${DAYTONA_SECRETS_VOLUME_ID}:${DAYTONA_SECRETS_MOUNT}"
|
|
--auto-stop 60
|
|
--public
|
|
--target us
|
|
)
|
|
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
|
|
DAYTONA_CREATE_ARGS+=(--volume "${DAYTONA_ARTIFACTS_VOLUME_ID}:${DAYTONA_ARTIFACTS_MOUNT}")
|
|
fi
|
|
daytona create "${DAYTONA_CREATE_ARGS[@]}"
|
|
|
|
echo ""
|
|
echo "==> Waiting for sandbox exec readiness..."
|
|
exec_ready=0
|
|
for _ in $(seq 1 60); do
|
|
if daytona exec "$SANDBOX" -- "bash -lc 'true'" >/dev/null 2>&1; then
|
|
exec_ready=1
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
if [ "$exec_ready" -ne 1 ]; then
|
|
echo "ERROR: sandbox did not become exec-ready" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
|
|
echo ""
|
|
echo "==> Starting artifacts download server..."
|
|
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; mkdir -p \"$DAYTONA_ARTIFACTS_MOUNT/recordings\" \"$DAYTONA_ARTIFACTS_MOUNT/screenshots\" \"$DAYTONA_ARTIFACTS_MOUNT/validation\"; nohup python3 -m http.server $ARTIFACTS_PORT --directory \"$DAYTONA_ARTIFACTS_MOUNT\" >/tmp/daytona-artifacts.log 2>&1 &'"
|
|
fi
|
|
|
|
echo ""
|
|
echo "==> Checking out $REF..."
|
|
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; REF=\"$REF\"; FORCE_INSTALL=\"$FORCE_INSTALL\"; PNPM_STORE=/workspace/.openwork-daytona/pnpm-store; if git fetch origin \"\$REF\"; then git checkout --detach FETCH_HEAD; else git fetch origin dev --depth 50 || true; git checkout \"\$REF\"; fi; git rev-parse --short HEAD; mkdir -p \"\$PNPM_STORE\" .openwork-daytona; baseline=.openwork-daytona/pnpm-lock.sha256; current=\$(sha256sum pnpm-lock.yaml | cut -d \" \" -f 1); if [ \"\$FORCE_INSTALL\" = 1 ] || [ ! -d node_modules ] || [ ! -f \"\$baseline\" ] || [ \"\$(cat \"\$baseline\")\" != \"\$current\" ]; then echo \"==> Installing deps (missing node_modules, lockfile changed, or forced)...\"; CI=1 pnpm install --store-dir \"\$PNPM_STORE\" --frozen-lockfile || CI=1 pnpm install --store-dir \"\$PNPM_STORE\"; printf \"%s\" \"\$current\" > \"\$baseline\"; else echo \"==> Skipping pnpm install (node_modules present and lockfile unchanged).\"; fi'"
|
|
|
|
echo ""
|
|
echo "==> Starting OpenWork sandbox dev stack..."
|
|
daytona exec "$SANDBOX" -- "bash -lc 'set -euo pipefail; cd /workspace; DEN_BASE_URL=\"$DEN_BASE_URL\"; DEN_API_BASE_URL=\"$DEN_API_BASE_URL\"; DEN_REQUIRE_SIGNIN=\"$DEN_REQUIRE_SIGNIN\"; if [ -n \"\$DEN_BASE_URL\" ] || [ -n \"\$DEN_API_BASE_URL\" ] || [ \"\$DEN_REQUIRE_SIGNIN\" = 1 ]; then mkdir -p /workspace/.openwork-daytona; DEN_BASE_URL=\"\$DEN_BASE_URL\" DEN_API_BASE_URL=\"\$DEN_API_BASE_URL\" DEN_REQUIRE_SIGNIN=\"\$DEN_REQUIRE_SIGNIN\" node -e '\''const fs = require(\"node:fs\"); const baseUrl = process.env.DEN_BASE_URL || \"https://app.openworklabs.com\"; const apiBaseUrl = process.env.DEN_API_BASE_URL || null; const requireSignin = process.env.DEN_REQUIRE_SIGNIN === \"1\"; fs.writeFileSync(\"/workspace/.openwork-daytona/desktop-bootstrap.json\", JSON.stringify({ baseUrl, apiBaseUrl, requireSignin }, null, 2) + \"\\n\");'\''; fi; export DAYTONA_SECRETS_ENV=\"$DAYTONA_SECRETS_ENV\" DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS=\"$DAYTONA_ELECTRON_EXTRA_LAUNCH_ARGS\" OPENWORK_ELECTRON_REMOTE_DEBUG_PORT=$CDP_PORT OPENWORK_ELECTRON_FAKE_MEDIA=\"$OPENWORK_ELECTRON_FAKE_MEDIA\" OPENWORK_WORKSPACE_DIR=/workspace OPENWORK_GOOGLE_WORKSPACE_ALLOW_PLAINTEXT_VAULT=1; if [ -f /workspace/.openwork-daytona/desktop-bootstrap.json ]; then export OPENWORK_DESKTOP_BOOTSTRAP_PATH=/workspace/.openwork-daytona/desktop-bootstrap.json; fi; pnpm dev:sandbox'"
|
|
|
|
echo ""
|
|
echo "==> Waiting for Electron CDP on port $CDP_PORT (up to ${MAX_WAIT}s)..."
|
|
elapsed=0
|
|
while [ "$elapsed" -lt "$MAX_WAIT" ]; do
|
|
if daytona exec "$SANDBOX" -- "bash -lc 'curl -sf http://127.0.0.1:$CDP_PORT/json/list >/dev/null 2>&1'" 2>/dev/null; then
|
|
echo " CDP ready after ${elapsed}s."
|
|
break
|
|
fi
|
|
sleep 5
|
|
elapsed=$((elapsed + 5))
|
|
printf " %ds...\r" "$elapsed"
|
|
done
|
|
|
|
if [ "$elapsed" -ge "$MAX_WAIT" ]; then
|
|
echo ""
|
|
echo "WARNING: CDP did not become ready within ${MAX_WAIT}s."
|
|
echo "Check logs:"
|
|
echo " daytona exec $SANDBOX -- 'tail -80 /tmp/start-vnc.log'"
|
|
echo " daytona exec $SANDBOX -- 'tail -80 /tmp/electron.log'"
|
|
echo " daytona exec $SANDBOX -- 'tail -80 /tmp/vite.log'"
|
|
exit 1
|
|
fi
|
|
|
|
RECORDING_PATH=""
|
|
if [ "$RECORD_VIDEO" -eq 1 ]; then
|
|
RECORDING_PATH="${DAYTONA_ARTIFACTS_MOUNT}/recordings/${RECORDING_NAME}.mp4"
|
|
echo ""
|
|
echo "==> Starting Daytona display recording..."
|
|
daytona exec "$SANDBOX" -- "bash -lc 'cd /workspace; DISPLAY=:99 .devcontainer/start-daytona-recording.sh --detach --output \"$RECORDING_PATH\" --size \"$RECORDING_SIZE\" --fps \"$RECORDING_FPS\"'"
|
|
echo " Recording is active. Begin the user-visible flow after the ready banner below."
|
|
fi
|
|
|
|
echo ""
|
|
if daytona exec "$SANDBOX" -- "bash -lc 'ps aux | grep opencode | grep -v grep'" 2>/dev/null | grep -q opencode; then
|
|
echo "==> opencode sidecar: running"
|
|
else
|
|
echo "==> opencode sidecar: not running (workspace may need creation first)"
|
|
fi
|
|
|
|
echo "==> Secrets env: ${DAYTONA_SECRETS_ENV} (file or directory, sourced if present)"
|
|
if [ -n "$DEN_BASE_URL" ]; then
|
|
echo "==> Den base URL: $DEN_BASE_URL"
|
|
fi
|
|
|
|
CDP_URL=$(daytona preview-url "$SANDBOX" -p "$CDP_PORT" 2>/dev/null | grep -v "^time=")
|
|
NOVNC_URL=$(daytona preview-url "$SANDBOX" -p "$NOVNC_PORT" 2>/dev/null | grep -v "^time=")
|
|
ARTIFACTS_URL=""
|
|
RECORDING_URL=""
|
|
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
|
|
ARTIFACTS_URL=$(daytona preview-url "$SANDBOX" -p "$ARTIFACTS_PORT" 2>/dev/null | grep -v "^time=")
|
|
fi
|
|
if [ "$RECORD_VIDEO" -eq 1 ]; then
|
|
RECORDING_URL="${ARTIFACTS_URL%/}/recordings/${RECORDING_NAME}.mp4"
|
|
fi
|
|
|
|
echo ""
|
|
echo "============================================"
|
|
echo " Sandbox ready: $SANDBOX"
|
|
echo ""
|
|
echo " Electron CDP: $CDP_URL"
|
|
echo " noVNC visual: $NOVNC_URL"
|
|
if [ "$ARTIFACTS_ENABLED" -eq 1 ]; then
|
|
echo " Artifacts: $ARTIFACTS_URL"
|
|
echo " Screenshot:"
|
|
echo " daytona exec $SANDBOX -- 'bash .devcontainer/capture-daytona-screenshot.sh'"
|
|
fi
|
|
if [ "$RECORD_VIDEO" -eq 1 ]; then
|
|
echo " Recording: $RECORDING_URL"
|
|
echo ""
|
|
echo " Stop recording:"
|
|
echo " daytona exec $SANDBOX -- 'bash .devcontainer/stop-daytona-recording.sh'"
|
|
echo " Validate recording duration before using it as evidence."
|
|
fi
|
|
echo ""
|
|
echo " Connect browser tools:"
|
|
echo " browser_list({ browser_url: \"$CDP_URL\" })"
|
|
echo ""
|
|
echo " Cleanup:"
|
|
echo " daytona delete $SANDBOX"
|
|
echo "============================================"
|