Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions .github/actions/deploy-dev-to-ec2/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: 'Deploy dev slot to EC2'
description: >
Deploy the web-client and cloud-services commits pinned by the checked-out branch to the dev
slot on EC2 (nginx :4174 -> backend-dev :8081). Expects the caller to have checked out the
branch with submodules. Prod (port 4173, backend service) is not touched.

inputs:
ec2_pem_key:
description: 'EC2 PEM key content for SSH authentication'
required: true
ec2_host:
description: 'EC2 host address'
required: true
ec2_user:
description: 'EC2 SSH user'
required: true
branch:
description: 'Branch being deployed (recorded in deploy-info.txt)'
required: true
dev_dir:
description: 'Directory on EC2 that holds the dev cloud-services copy'
required: false
default: '/home/ubuntu/dev'
prod_cloud_services_dir:
description: 'Prod cloud-services directory on EC2; its env file seeds the dev env file on first deploy'
required: false
default: '/home/ubuntu/cloud-services'
web_api_base_url:
description: 'API URL baked into the dev web-client build; empty means the page origin, which nginx proxies to backend-dev'
required: false
default: ''

runs:
using: 'composite'
steps:
# Builds exactly the web-client commit this branch pins, on the runner (EC2 is too small for vite build)
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'web-client/package-lock.json'

- name: Build web-client and package cloud-services
shell: bash
env:
WEB_API_BASE_URL: ${{ inputs.web_api_base_url }}
BRANCH: ${{ inputs.branch }}
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/web-client"
# Set even when empty so it overrides web-client/.env.production (which points at prod)
export VITE_API_BASE_URL="$WEB_API_BASE_URL"
npm ci
npm run build

cd "$GITHUB_WORKSPACE"
{
echo "branch: $BRANCH"
echo "gui commit: $(git rev-parse HEAD)"
echo "web-client: $(git -C web-client rev-parse HEAD)"
echo "cloud-services: $(git -C cloud-services rev-parse HEAD)"
echo "deployed at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
} > web-client/dist/deploy-info.txt
cat web-client/dist/deploy-info.txt

tar -czf "$RUNNER_TEMP/web-client-dev.tgz" -C web-client/dist .
tar -czf "$RUNNER_TEMP/cloud-services-dev.tgz" -C cloud-services \
--exclude=.git --exclude=venv --exclude=.venv --exclude=__pycache__ --exclude='.env*' .

- name: Configure SSH
shell: bash
run: |
mkdir -p ~/.ssh
echo "${{ inputs.ec2_pem_key }}" > ~/.ssh/ec2_deploy.pem
chmod 600 ~/.ssh/ec2_deploy.pem
cat >> ~/.ssh/config << EOF
Host ec2-deploy
HostName ${{ inputs.ec2_host }}
User ${{ inputs.ec2_user }}
IdentityFile ~/.ssh/ec2_deploy.pem
StrictHostKeyChecking no
EOF

- name: Upload build, source and configs
shell: bash
run: |
scp "$RUNNER_TEMP/web-client-dev.tgz" "$RUNNER_TEMP/cloud-services-dev.tgz" \
"$GITHUB_WORKSPACE/deploy/nginx/hyperloop-gui-dev.conf" \
"$GITHUB_WORKSPACE/deploy/systemd/backend-dev.service" \
ec2-deploy:/tmp/

- name: Deploy dev slot
shell: bash
run: |
ssh ec2-deploy << 'DEPLOY_EOF'
set -euo pipefail

DEV_DIR="${{ inputs.dev_dir }}"
PROD_APP="${{ inputs.prod_cloud_services_dir }}"
APP="$DEV_DIR/cloud-services"
WEB_ROOT=/var/www/hyperloop-gui-dev

# --- Web client (nginx :4174) ---
echo "Deploying dev web-client..."
if ! command -v nginx >/dev/null; then
sudo apt-get update -q
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q nginx
sudo rm -f /etc/nginx/sites-enabled/default
fi
sudo rm -rf "$WEB_ROOT.new" "$WEB_ROOT.old"
sudo mkdir -p "$WEB_ROOT.new"
sudo tar -xzf /tmp/web-client-dev.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-dev.tgz

sudo install -m 644 /tmp/hyperloop-gui-dev.conf /etc/nginx/sites-available/hyperloop-gui-dev
sudo ln -sfn /etc/nginx/sites-available/hyperloop-gui-dev /etc/nginx/sites-enabled/hyperloop-gui-dev
rm -f /tmp/hyperloop-gui-dev.conf
sudo nginx -t
sudo systemctl reload-or-restart nginx

# --- Cloud services (backend-dev :8081) ---
echo "Deploying dev cloud-services..."
mkdir -p "$APP"
# Replace the source but keep the venv and the dev env file
find "$APP" -mindepth 1 -maxdepth 1 ! -name venv ! -name .env.dev -exec rm -rf {} +
tar -xzf /tmp/cloud-services-dev.tgz -C "$APP"
rm -f /tmp/cloud-services-dev.tgz

# First deploy only: start from prod's settings so the same logins and hub tokens work.
# Edit $APP/.env.dev afterwards to diverge (it is never overwritten).
if [ ! -f "$APP/.env.dev" ]; then
SEED=""
for f in "$PROD_APP/.env.production" "$PROD_APP/.env"; do
if [ -f "$f" ]; then SEED="$f"; break; fi
done
if [ -n "$SEED" ]; then
echo "Seeding .env.dev from $SEED"
cp "$SEED" "$APP/.env.dev"
else
echo "No prod env file found; dev runs with development defaults"
: > "$APP/.env.dev"
fi
chmod 600 "$APP/.env.dev"
fi

cd "$APP"
if [ ! -x venv/bin/python ]; then
echo "Creating virtual environment..."
python3 -m venv venv
fi
# Reinstall only when requirements.txt changes
REQ_HASH="$(sha256sum requirements.txt | cut -d' ' -f1)"
if [ "$(cat venv/.requirements.sha256 2>/dev/null || true)" != "$REQ_HASH" ]; then
venv/bin/python -m pip install -q --upgrade pip setuptools wheel
venv/bin/pip install -q -r requirements.txt
echo "$REQ_HASH" > venv/.requirements.sha256
fi

sed -e "s|@APP_DIR@|$APP|g" -e "s|@USER@|$(id -un)|g" /tmp/backend-dev.service \
| sudo tee /etc/systemd/system/backend-dev.service > /dev/null
rm -f /tmp/backend-dev.service
sudo systemctl daemon-reload
sudo systemctl enable backend-dev
sudo systemctl restart backend-dev

echo "Waiting for backend-dev..."
for i in $(seq 1 20); do
if curl -fsS http://127.0.0.1:8081/health > /dev/null; then
echo "✅ Dev deployment completed: backend-dev healthy"
exit 0
fi
sleep 1
done
echo "❌ backend-dev did not become healthy"
sudo journalctl -u backend-dev -n 50 --no-pager
exit 1
DEPLOY_EOF
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI/CD Pipeline

on:
pull_request:
branches: [ "main" ]
branches: [ "main", "test/**" ]
types: [opened, synchronize, reopened, closed]

jobs:
Expand Down Expand Up @@ -34,10 +34,19 @@ jobs:

# Deploy when PR is merged to main
deploy:
if: github.event.pull_request.merged == true && github.event.action == 'closed'
if: github.event.pull_request.merged == true && github.event.action == 'closed' && github.base_ref == 'main'
needs: [test-api, test-web]
uses: ./.github/workflows/deploy.yml
with:
branch: 'main'
repo_base_dir: '/home/ubuntu'
secrets: inherit

# Deploy to the dev slot (port 4174) when a PR is merged into a test/* branch
deploy-dev:
if: github.event.pull_request.merged == true && github.event.action == 'closed' && startsWith(github.base_ref, 'test/')
needs: [test-api, test-web]
uses: ./.github/workflows/deploy-dev.yml
with:
branch: ${{ github.base_ref }}
secrets: inherit
50 changes: 50 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Deploy dev slot to EC2

on:
# Called by ci.yml when a PR is merged into a test/* branch
workflow_call:
inputs:
branch:
description: 'Branch to deploy to the dev slot'
required: true
type: string
secrets:
EC2_PEM_KEY:
required: true
EC2_HOST:
required: true
EC2_USER:
required: true
# Manual redeploy from the Actions tab (any branch, e.g. a test/* branch you pushed to directly)
workflow_dispatch:
inputs:
branch:
description: 'Branch to deploy to the dev slot (e.g. test/my-feature)'
required: true
type: string

jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy dev slot
# One dev slot: let deploys finish in order instead of racing on the server
concurrency:
group: deploy-dev
cancel-in-progress: false

steps:
- name: Checkout branch with pinned submodules
uses: actions/checkout@v5
with:
ref: ${{ inputs.branch }}
submodules: recursive
token: ${{ github.token }}

- name: Deploy dev slot
uses: ./.github/actions/deploy-dev-to-ec2
with:
ec2_pem_key: ${{ secrets.EC2_PEM_KEY }}
ec2_host: ${{ secrets.EC2_HOST }}
ec2_user: ${{ secrets.EC2_USER }}
branch: ${{ inputs.branch }}
web_api_base_url: ${{ vars.DEV_WEB_API_BASE_URL }}
53 changes: 53 additions & 0 deletions deploy/nginx/hyperloop-gui-dev.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Dev slot: serves the web-client built from the latest merged test/* branch and proxies
# the dev cloud-services instance (backend-dev, 127.0.0.1:8081) on the same origin.
# Mirrors hyperloop-gui.conf; only the port, web root and upstream differ.
# The deploy-dev-to-ec2 action installs this as /etc/nginx/sites-available/hyperloop-gui-dev.

map $http_upgrade $hyperloop_dev_connection_upgrade {
default upgrade;
'' close;
}

server {
listen 4174;
server_name _;

root /var/www/hyperloop-gui-dev;
index index.html;

gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;

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_dev_connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1h;
proxy_send_timeout 1h;

location /auth/ { proxy_pass http://127.0.0.1:8081; }
location /api/ { proxy_pass http://127.0.0.1:8081; }
location /ws/ { proxy_pass http://127.0.0.1:8081; }
location = /hub { proxy_pass http://127.0.0.1:8081; }
location = /health { proxy_pass http://127.0.0.1:8081; }

# Which branch and commits are deployed here
location = /deploy-info.txt {
add_header Cache-Control "no-cache";
default_type text/plain;
}

location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}

location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}
}
18 changes: 18 additions & 0 deletions deploy/systemd/backend-dev.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Dev cloud-services instance for test/* branches. Installed by the deploy-dev-to-ec2 action,
# which replaces @APP_DIR@ and @USER@. Kept on localhost; nginx (hyperloop-gui-dev) proxies it.
[Unit]
Description=Hyperloop cloud-services (dev slot, test/* branches)
After=network.target

[Service]
User=@USER@
WorkingDirectory=@APP_DIR@
Environment=ENV_FILE=.env.dev
ExecStart=@APP_DIR@/venv/bin/uvicorn src.main:app --host 127.0.0.1 --port 8081
Restart=on-failure
RestartSec=3
# The instance is small; keep a misbehaving dev build from starving prod
MemoryMax=300M

[Install]
WantedBy=multi-user.target
Loading