diff --git a/.github/actions/deploy-to-ec2/action.yml b/.github/actions/deploy-to-ec2/action.yml index 5322ace..b73b4a8 100644 --- a/.github/actions/deploy-to-ec2/action.yml +++ b/.github/actions/deploy-to-ec2/action.yml @@ -1,5 +1,5 @@ name: 'Deploy to EC2' -description: 'Deploy application to EC2 using SSH with venv support' +description: 'Build the web client on the runner, then deploy it and cloud-services to EC2 over SSH' inputs: ec2_pem_key: @@ -12,17 +12,48 @@ inputs: description: 'EC2 SSH user' required: true repo_base_dir: - description: 'Base directory on EC2 where cloud-services and web-client are located' + description: 'Base directory on EC2 where cloud-services is located' required: false default: '/home/ubuntu' branch: description: 'Git branch to deploy' required: false default: 'main' + web_api_base_url: + description: 'API URL baked into the web-client build; empty means the page origin, which nginx proxies to cloud-services' + required: false + default: '' runs: using: 'composite' steps: + # The EC2 instance has too little memory for `vite build`, so the runner builds the bundle. + - name: Check out web-client + uses: actions/checkout@v5 + with: + repository: hyperloop-cornell/web-client + ref: ${{ inputs.branch }} + path: .deploy/web-client + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: '.deploy/web-client/package-lock.json' + + - name: Build web-client + shell: bash + working-directory: .deploy/web-client + env: + WEB_API_BASE_URL: ${{ inputs.web_api_base_url }} + run: | + # Set even when empty so it overrides web-client/.env.production + export VITE_API_BASE_URL="$WEB_API_BASE_URL" + npm ci + npm run build + tar -czf "$RUNNER_TEMP/web-client-dist.tgz" -C dist . + - name: Configure SSH shell: bash run: | @@ -37,6 +68,10 @@ runs: StrictHostKeyChecking no EOF + - name: Upload web-client build and nginx config + shell: bash + run: scp "$RUNNER_TEMP/web-client-dist.tgz" "$GITHUB_WORKSPACE/deploy/nginx/hyperloop-gui.conf" ec2-deploy:/tmp/ + - name: Deploy to EC2 shell: bash run: | @@ -46,23 +81,36 @@ runs: BASE_DIR="${{ inputs.repo_base_dir }}" BRANCH="${{ inputs.branch }}" - # Deploy Web Client + # Deploy Web Client: nginx serves the build and proxies the API (deploy/nginx/hyperloop-gui.conf) echo "Deploying web-client..." - cd "$BASE_DIR/web-client" - - # Update repository - git fetch origin - git checkout "$BRANCH" - git pull origin "$BRANCH" - - # Build web client - npm ci - npm run build - - # Restart frontend service - echo "Restarting frontend service..." - sudo systemctl restart frontend - + if ! command -v nginx >/dev/null; then + sudo apt-get update -q + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q nginx + # The stock site listens on :80, which this deployment does not use + sudo rm -f /etc/nginx/sites-enabled/default + fi + + WEB_ROOT=/var/www/hyperloop-gui + sudo rm -rf "$WEB_ROOT.new" "$WEB_ROOT.old" + sudo mkdir -p "$WEB_ROOT.new" + sudo tar -xzf /tmp/web-client-dist.tgz --no-same-owner -C "$WEB_ROOT.new" + if [ -d "$WEB_ROOT" ]; then sudo mv "$WEB_ROOT" "$WEB_ROOT.old"; fi + sudo mv "$WEB_ROOT.new" "$WEB_ROOT" + sudo rm -rf "$WEB_ROOT.old" + rm -f /tmp/web-client-dist.tgz + + sudo install -m 644 /tmp/hyperloop-gui.conf /etc/nginx/sites-available/hyperloop-gui + sudo ln -sfn /etc/nginx/sites-available/hyperloop-gui /etc/nginx/sites-enabled/hyperloop-gui + rm -f /tmp/hyperloop-gui.conf + sudo nginx -t + + # nginx takes over port 4173 from the old `vite preview` service + if systemctl cat frontend >/dev/null 2>&1; then + sudo systemctl disable --now frontend + fi + sudo systemctl enable nginx + sudo systemctl reload-or-restart nginx + # Deploy Cloud Services API with venv echo "Deploying cloud-services..." cd "$BASE_DIR/cloud-services" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 98e6ff2..963dada 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,7 @@ on: type: string default: 'main' repo_base_dir: - description: 'Base directory on EC2 where cloud-services and web-client are located' + description: 'Base directory on EC2 where cloud-services is located' required: false type: string default: '/home/ubuntu' @@ -41,3 +41,4 @@ jobs: ec2_user: ${{ secrets.EC2_USER }} repo_base_dir: ${{ inputs.repo_base_dir }} branch: ${{ inputs.branch }} + web_api_base_url: ${{ vars.WEB_API_BASE_URL }} diff --git a/deploy/nginx/hyperloop-gui.conf b/deploy/nginx/hyperloop-gui.conf new file mode 100644 index 0000000..49aecce --- /dev/null +++ b/deploy/nginx/hyperloop-gui.conf @@ -0,0 +1,50 @@ +# Serves the web-client build and proxies cloud-services on the same origin, so the +# browser needs no CORS and port 8080 does not have to be reachable from the internet. +# The deploy action installs this as /etc/nginx/sites-available/hyperloop-gui. + +map $http_upgrade $hyperloop_connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 4173; + server_name _; + + root /var/www/hyperloop-gui; + index index.html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + + # Flash requests carry the firmware base64-encoded in the JSON body + client_max_body_size 25m; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $hyperloop_connection_upgrade; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Hub and browser websockets can sit idle between messages + proxy_read_timeout 1h; + proxy_send_timeout 1h; + + location /auth/ { proxy_pass http://127.0.0.1:8080; } + location /api/ { proxy_pass http://127.0.0.1:8080; } + location /ws/ { proxy_pass http://127.0.0.1:8080; } + location = /hub { proxy_pass http://127.0.0.1:8080; } + location = /health { proxy_pass http://127.0.0.1:8080; } + + # File names under /assets/ change with their content on every build + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + # Client-side routes (/devices, /telemetry, ...) load the app shell + location / { + add_header Cache-Control "no-cache"; + try_files $uri $uri/ /index.html; + } +}