mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
103 lines
5 KiB
Bash
103 lines
5 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# Open SWE app deploy — RE-RUNNABLE (first boot + every subsequent release).
|
||
|
|
#
|
||
|
|
# Pulls the current release from S3 (open-swe-<env>-assets), builds the venv
|
||
|
|
# natively on the box, and restarts the service. This is the SINGLE source of the
|
||
|
|
# app-deploy procedure; it runs in two places:
|
||
|
|
#
|
||
|
|
# 1. first boot — user-data.sh decodes this script to /opt/open-swe/bin and
|
||
|
|
# calls it ONCE (non-fatal: if no release is published yet,
|
||
|
|
# nginx is already up and the box waits for the first deploy).
|
||
|
|
# 2. every release — the `open-swe-<env>-deploy` SSM document (CI fires it after
|
||
|
|
# uploading app.tar.gz / spa.tar.gz) runs this same script.
|
||
|
|
#
|
||
|
|
# It deploys CODE + STATIC ASSETS only. Secrets/config are NOT fetched here: the
|
||
|
|
# systemd unit's ExecStartPre=fetch-config.sh materializes the tmpfs .env on every
|
||
|
|
# (re)start, fail-fast — so `systemctl restart` below is what reloads config too.
|
||
|
|
#
|
||
|
|
# Contract:
|
||
|
|
# app.tar.gz = the Python source tree (pyproject.toml + uv.lock + agent/ +
|
||
|
|
# deploy/ + langgraph.json + README.md, NO ui/, NO .venv). The venv
|
||
|
|
# is built HERE with `uv sync` so it is native ARM64 and lives at
|
||
|
|
# the real runtime path (no cross-built / non-relocatable venv).
|
||
|
|
# spa.tar.gz = the built dashboard SPA (vite output: _shell.html + assets),
|
||
|
|
# extracted to the nginx web root.
|
||
|
|
set -euo pipefail
|
||
|
|
exec > >(tee -a /var/log/open-swe/deploy.log) 2>&1
|
||
|
|
echo "==> open-swe deploy start $(date -u +%FT%TZ)"
|
||
|
|
|
||
|
|
# Non-secret pointers written by user-data.sh (env, region, bucket, artifact prefix).
|
||
|
|
# shellcheck disable=SC1091
|
||
|
|
. /etc/open-swe/boot.env
|
||
|
|
export AWS_DEFAULT_REGION="${AWS_REGION:?boot.env missing AWS_REGION}"
|
||
|
|
: "${ASSETS_BUCKET:?boot.env missing ASSETS_BUCKET}"
|
||
|
|
: "${ARTIFACT_PREFIX:?boot.env missing ARTIFACT_PREFIX}"
|
||
|
|
|
||
|
|
# Fixed layout — must match provision.sh + user-data.sh + the templates.
|
||
|
|
SERVICE_USER="openswe"
|
||
|
|
APP_DIR="/opt/open-swe/app"
|
||
|
|
WWW_ROOT="/var/www/open-swe"
|
||
|
|
ENV_FILE="/run/open-swe/.env"
|
||
|
|
UV_BIN="/usr/local/bin/uv"
|
||
|
|
UV_PYTHON_INSTALL_DIR="/opt/uv/python" # where provision.sh pre-installed py3.12
|
||
|
|
SERVICE_HOME="/opt/open-swe"
|
||
|
|
|
||
|
|
# Benign-vs-failure distinction: on a brand-new env no release is published yet.
|
||
|
|
# Treat "app.tar.gz absent in S3" as a benign no-op (exit 0) so first boot is not a
|
||
|
|
# scary failure; ONCE a release exists, any later step failing is loud (set -e).
|
||
|
|
if ! aws s3 ls "s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/app.tar.gz" >/dev/null 2>&1; then
|
||
|
|
echo "==> no release published at s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/ yet — nothing to deploy"
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "==> pull release from s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/"
|
||
|
|
tmp="$(mktemp -d)"
|
||
|
|
trap 'rm -rf "$tmp"' EXIT
|
||
|
|
aws s3 cp "s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/app.tar.gz" "${tmp}/app.tar.gz"
|
||
|
|
aws s3 cp "s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/spa.tar.gz" "${tmp}/spa.tar.gz"
|
||
|
|
|
||
|
|
# Replace app source + SPA atomically-ish: clear the dirs (drops files removed in
|
||
|
|
# this release) then extract. The venv is rebuilt below, so wiping .venv too is
|
||
|
|
# fine — uv's cache (in the service home) makes the rebuild fast.
|
||
|
|
#
|
||
|
|
# Hardening: deploy.sh runs as root, so extract with --no-same-owner
|
||
|
|
# --no-same-permissions — files take root:root + umask perms (NOT the archive's
|
||
|
|
# uid/mode), so a tarball cannot land a setuid/setgid binary or a foreign-owned
|
||
|
|
# file; the chown -R below then hands the tree to the service user. (GNU tar also
|
||
|
|
# refuses `..`-escaping members by default.) Defense-in-depth: the only writer of
|
||
|
|
# this bucket is the CI OIDC app role, but the box never trusts the archive's
|
||
|
|
# ownership/mode regardless.
|
||
|
|
echo "==> install app source -> ${APP_DIR}"
|
||
|
|
install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 "$APP_DIR" "$WWW_ROOT"
|
||
|
|
find "$APP_DIR" -mindepth 1 -delete
|
||
|
|
find "$WWW_ROOT" -mindepth 1 -delete
|
||
|
|
tar --no-same-owner --no-same-permissions -xzf "${tmp}/app.tar.gz" -C "$APP_DIR"
|
||
|
|
tar --no-same-owner --no-same-permissions -xzf "${tmp}/spa.tar.gz" -C "$WWW_ROOT"
|
||
|
|
# langgraph reads ./.env from WorkingDirectory; point it at the tmpfs file the
|
||
|
|
# systemd ExecStartPre materializes.
|
||
|
|
ln -sfn "$ENV_FILE" "${APP_DIR}/.env"
|
||
|
|
chown -R "$SERVICE_USER":"$SERVICE_USER" "$APP_DIR" "$WWW_ROOT"
|
||
|
|
|
||
|
|
echo "==> build venv natively (uv sync --frozen --no-dev)"
|
||
|
|
# Run as the service user so the venv + uv cache are owned by it. Pin the
|
||
|
|
# pre-baked interpreter dir so uv never reaches out to download Python at deploy.
|
||
|
|
cd "$APP_DIR"
|
||
|
|
sudo -u "$SERVICE_USER" env \
|
||
|
|
HOME="$SERVICE_HOME" \
|
||
|
|
UV_PYTHON_INSTALL_DIR="$UV_PYTHON_INSTALL_DIR" \
|
||
|
|
UV_CACHE_DIR="${SERVICE_HOME}/.cache/uv" \
|
||
|
|
"$UV_BIN" sync --frozen --no-dev
|
||
|
|
|
||
|
|
echo "==> restart open-swe.service + reload nginx"
|
||
|
|
# ExecStartPre=fetch-config.sh fails-fast if secrets/config are missing, so a
|
||
|
|
# restart here surfaces a bad config as a failed unit (non-zero exit below).
|
||
|
|
systemctl restart open-swe.service
|
||
|
|
nginx -t && systemctl reload nginx
|
||
|
|
|
||
|
|
if systemctl is-active --quiet open-swe.service; then
|
||
|
|
echo "==> open-swe deploy OK $(date -u +%FT%TZ)"
|
||
|
|
else
|
||
|
|
echo "!! open-swe.service is not active after deploy (check fetch-config/secrets)"
|
||
|
|
exit 1
|
||
|
|
fi
|