547 lines
15 KiB
Markdown
547 lines
15 KiB
Markdown
# Auto-Deploy Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** One-command auto-deploy script that detects git changes and deploys only modified components. Clean up obsolete files.
|
||
|
||
**Architecture:** One bash script (`deploy-auto.sh`) replaces the existing `deploy-ssh.sh`. It uses `git diff --name-only` against a saved commit hash (`.deployed-commit`) to decide what changed, then reuses existing rsync/tar transport logic. A markdown guide (`SERVER_SETUP.md`) replaces the bootstrap script.
|
||
|
||
**Tech Stack:** Bash, git, rsync/tar, SSH, nginx, systemd, Node.js
|
||
|
||
---
|
||
|
||
### Task 1: Delete obsolete files
|
||
|
||
**Files:**
|
||
- Delete: `scripts/deploy-ssh.sh`
|
||
- Delete: `scripts/deploy-ssh.ps1`
|
||
- Delete: `scripts/deploy.env.example`
|
||
- Delete: `scripts/read-deploy-env.ps1`
|
||
- Delete: `scripts/register-ssh-key-for-root.ps1`
|
||
- Delete: `scripts/complete-lan-deploy.ps1`
|
||
- Delete: `scripts/craftshop-remote-lan.env`
|
||
- Delete: `scripts/server-bootstrap.sh`
|
||
- Delete: `docs/deploy-changes.md`
|
||
- Delete: `docs/test-deploy-proxmox.md`
|
||
- Delete: `docs/nginx-upload-limit.md`
|
||
|
||
- [ ] **Step 1: Remove all obsolete files**
|
||
|
||
```bash
|
||
git rm scripts/deploy-ssh.sh \
|
||
scripts/deploy-ssh.ps1 \
|
||
scripts/deploy.env.example \
|
||
scripts/read-deploy-env.ps1 \
|
||
scripts/register-ssh-key-for-root.ps1 \
|
||
scripts/complete-lan-deploy.ps1 \
|
||
scripts/craftshop-remote-lan.env \
|
||
scripts/server-bootstrap.sh \
|
||
docs/deploy-changes.md \
|
||
docs/test-deploy-proxmox.md \
|
||
docs/nginx-upload-limit.md
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add -A && git commit -m "chore: remove obsolete deploy scripts and docs"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Add .deployed-commit to .gitignore
|
||
|
||
**Files:**
|
||
- Modify: `.gitignore`
|
||
|
||
- [ ] **Step 1: Add .deployed-commit to .gitignore**
|
||
|
||
Add line at the end of `.gitignore`:
|
||
|
||
```
|
||
.deployed-commit
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add .gitignore && git commit -m "chore: add .deployed-commit to gitignore"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Write deploy-auto.sh
|
||
|
||
**Files:**
|
||
- Create: `scripts/deploy-auto.sh`
|
||
|
||
- [ ] **Step 1: Create deploy-auto.sh with full implementation**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
# Auto-deploy: детект изменений через git diff, сборка и деплой только изменённых компонентов.
|
||
#
|
||
# Зависимости: bash, git, ssh; rsync (Linux/macOS) или tar (Git Bash).
|
||
# Конфиг: scripts/deploy.env (скопируйте из deploy.env.example).
|
||
#
|
||
# Примеры:
|
||
# ./scripts/deploy-auto.sh # деплой изменений
|
||
# ./scripts/deploy-auto.sh --force # полный деплой всех компонентов
|
||
# ./scripts/deploy-auto.sh -f # только фронт
|
||
# ./scripts/deploy-auto.sh -b # только бэкенд
|
||
|
||
set -euo pipefail
|
||
|
||
case "$(uname -s 2>/dev/null)" in
|
||
MINGW* | MSYS*) export MSYS2_ARG_CONV_EXCL="*" ;;
|
||
esac
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
||
# --- Config ---
|
||
if [[ -f "$SCRIPT_DIR/deploy.env" ]]; then
|
||
set -a
|
||
source "$SCRIPT_DIR/deploy.env"
|
||
set +a
|
||
fi
|
||
|
||
DEPLOY_HOST="${DEPLOY_HOST:-}"
|
||
DEPLOY_USER="${DEPLOY_USER:-root}"
|
||
DEPLOY_PATH="${DEPLOY_PATH:-/opt/craftshop}"
|
||
DEPLOY_FRONTEND_DIST="${DEPLOY_FRONTEND_DIST:-$DEPLOY_PATH/www}"
|
||
DEPLOY_SSH_IDENTITY="${DEPLOY_SSH_IDENTITY:-}"
|
||
DEPLOY_RESTART_CMD="${DEPLOY_RESTART_CMD:-systemctl restart craftshop-api}"
|
||
DEPLOY_SERVER_OWNER="${DEPLOY_SERVER_OWNER:-deploy}"
|
||
DEPLOY_SKIP_CHOWN="${DEPLOY_SKIP_CHOWN:-0}"
|
||
|
||
RSYNC_OPTS=(-az --delete --human-readable --progress)
|
||
SSH_OPTS=()
|
||
if [[ -n "${DEPLOY_SSH_IDENTITY}" ]]; then
|
||
SSH_OPTS+=(-i "$DEPLOY_SSH_IDENTITY")
|
||
fi
|
||
|
||
REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}"
|
||
SSH_BASE=(ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new "$REMOTE")
|
||
|
||
# --- Flags ---
|
||
FORCE=false
|
||
TARGET="auto"
|
||
|
||
usage() {
|
||
cat <<USAGE
|
||
Использование: $(basename "$0") [опции]
|
||
|
||
--force Деплой всех компонентов (игнорировать diff)
|
||
--frontend-only | -f Только клиент
|
||
--backend-only | -b Только сервер
|
||
--help | -h Помощь
|
||
|
||
Окружение (или scripts/deploy.env):
|
||
DEPLOY_HOST хост SSH (обязательно)
|
||
DEPLOY_PATH корень приложения на сервере (по умолчанию: /opt/craftshop)
|
||
DEPLOY_SSH_IDENTITY путь к приватному ключу (-i ssh)
|
||
DEPLOY_RESTART_CMD команда после бэкенд-деплоя
|
||
USAGE
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--force) FORCE=true ;;
|
||
--frontend-only | -f) TARGET="frontend" ;;
|
||
--backend-only | -b) TARGET="backend" ;;
|
||
-h | --help) usage; exit 0 ;;
|
||
*) echo "Неизвестный аргумент: $1" >&2; usage >&2; exit 1 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
if [[ -z "$DEPLOY_HOST" ]]; then
|
||
echo "Укажите DEPLOY_HOST (или добавьте в scripts/deploy.env)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# --- Helpers ---
|
||
remote_exec() {
|
||
"${SSH_BASE[@]}" "$@"
|
||
}
|
||
|
||
should_use_tar_transport() {
|
||
case "$(uname -s 2>/dev/null)" in
|
||
MINGW*|MSYS*|CYGWIN_NT*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
build_rsync_rsh() {
|
||
printf '%q ' ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new
|
||
}
|
||
|
||
# --- Diff detection ---
|
||
changed_client=false
|
||
changed_server=false
|
||
|
||
if [[ "$TARGET" == "frontend" ]]; then
|
||
changed_client=true
|
||
elif [[ "$TARGET" == "backend" ]]; then
|
||
changed_server=true
|
||
elif [[ "$FORCE" == true ]]; then
|
||
changed_client=true
|
||
changed_server=true
|
||
else
|
||
DEPLOYED_FILE="$ROOT/.deployed-commit"
|
||
if [[ -f "$DEPLOYED_FILE" ]]; then
|
||
LAST_DEPLOYED=$(cat "$DEPLOYED_FILE")
|
||
else
|
||
LAST_DEPLOYED="HEAD~1"
|
||
fi
|
||
|
||
echo ">>> Diff: $LAST_DEPLOYED..HEAD"
|
||
CHANGED_FILES=$(git -C "$ROOT" diff --name-only "$LAST_DEPLOYED" HEAD 2>/dev/null || true)
|
||
|
||
if echo "$CHANGED_FILES" | grep -q "^client/"; then
|
||
changed_client=true
|
||
fi
|
||
if echo "$CHANGED_FILES" | grep -q "^server/"; then
|
||
changed_server=true
|
||
fi
|
||
if echo "$CHANGED_FILES" | grep -q "^shared/"; then
|
||
changed_client=true
|
||
changed_server=true
|
||
fi
|
||
|
||
if [[ "$changed_client" == false && "$changed_server" == false ]]; then
|
||
echo ">>> Ничего не изменилось с последнего деплоя."
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
echo ">>> Клиент: $changed_client, Сервер: $changed_server"
|
||
|
||
# --- Deploy: Client ---
|
||
if [[ "$changed_client" == true ]]; then
|
||
echo ">>> Сборка клиента..."
|
||
(cd "$ROOT/client" && npm ci && npm run build)
|
||
|
||
remote_exec mkdir -p "$DEPLOY_FRONTEND_DIST"
|
||
|
||
if should_use_tar_transport; then
|
||
remote_exec "find ${DEPLOY_FRONTEND_DIST} -mindepth 1 -delete 2>/dev/null || true"
|
||
(cd "$ROOT/client/dist" && tar -czf - .) | \
|
||
"${SSH_BASE[@]}" "mkdir -p ${DEPLOY_FRONTEND_DIST} && tar xzf - -C ${DEPLOY_FRONTEND_DIST}"
|
||
else
|
||
local rsh
|
||
rsh="$(build_rsync_rsh)"
|
||
rsync "${RSYNC_OPTS[@]}" -e "$rsh" \
|
||
"$ROOT/client/dist/" "${REMOTE}:${DEPLOY_FRONTEND_DIST}/"
|
||
fi
|
||
|
||
echo ">>> Клиент задеплоен"
|
||
fi
|
||
|
||
# --- Deploy: Server ---
|
||
if [[ "$changed_server" == true ]]; then
|
||
remote_exec mkdir -p "$DEPLOY_PATH/server" "$DEPLOY_PATH/shared"
|
||
|
||
if should_use_tar_transport; then
|
||
(cd "$ROOT/server" && tar -czf - \
|
||
--exclude=node_modules --exclude=uploads --exclude=.git \
|
||
--exclude='*.db' --exclude=.env --exclude=.dev_env \
|
||
.) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_PATH}/server && tar xzf - -C ${DEPLOY_PATH}/server"
|
||
|
||
(cd "$ROOT/shared" && tar -czf - \
|
||
--exclude=.git \
|
||
.) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_PATH}/shared && tar xzf - -C ${DEPLOY_PATH}/shared"
|
||
else
|
||
local rsh
|
||
rsh="$(build_rsync_rsh)"
|
||
rsync "${RSYNC_OPTS[@]}" -e "$rsh" \
|
||
--exclude node_modules --exclude uploads --exclude .git \
|
||
--exclude '*.db' --exclude .env --exclude .dev_env \
|
||
"$ROOT/server/" "${REMOTE}:${DEPLOY_PATH}/server/"
|
||
|
||
rsync "${RSYNC_OPTS[@]}" -e "$rsh" \
|
||
--exclude .git \
|
||
"$ROOT/shared/" "${REMOTE}:${DEPLOY_PATH}/shared/"
|
||
fi
|
||
|
||
echo ">>> Сервер: npm ci, prisma generate, migrate deploy"
|
||
remote_exec bash -lc "set -e
|
||
cd \"$DEPLOY_PATH/server\"
|
||
npm ci
|
||
npx prisma generate
|
||
npx prisma migrate deploy
|
||
"
|
||
|
||
if [[ "${DEPLOY_USER}" == "root" && "${DEPLOY_SKIP_CHOWN}" != "1" ]]; then
|
||
echo ">>> chown ${DEPLOY_SERVER_OWNER}"
|
||
remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/server"
|
||
remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/shared"
|
||
fi
|
||
|
||
if [[ -n "${DEPLOY_RESTART_CMD}" ]]; then
|
||
echo ">>> Рестарт: $DEPLOY_RESTART_CMD"
|
||
remote_exec bash -lc "$DEPLOY_RESTART_CMD"
|
||
fi
|
||
|
||
echo ">>> Сервер задеплоен"
|
||
fi
|
||
|
||
# --- Save deployed commit ---
|
||
if [[ "$TARGET" == "auto" && "$FORCE" != true ]]; then
|
||
CURRENT_HEAD=$(git -C "$ROOT" rev-parse HEAD)
|
||
echo "$CURRENT_HEAD" > "$ROOT/.deployed-commit"
|
||
echo ">>> Сохранён коммит $CURRENT_HEAD"
|
||
fi
|
||
|
||
echo ">>> Готово."
|
||
```
|
||
|
||
- [ ] **Step 2: Make executable**
|
||
|
||
```bash
|
||
chmod +x scripts/deploy-auto.sh
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add scripts/deploy-auto.sh && git commit -m "feat: add deploy-auto.sh with git diff detection"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: Write SERVER_SETUP.md
|
||
|
||
**Files:**
|
||
- Create: `scripts/SERVER_SETUP.md`
|
||
|
||
- [ ] **Step 1: Create SERVER_SETUP.md**
|
||
|
||
```markdown
|
||
# Первичная настройка LXC для Craftshop
|
||
|
||
Выполнять от **root** на свежем Debian/Ubuntu LXC.
|
||
|
||
---
|
||
|
||
## 1. Базовые пакеты и Node.js
|
||
|
||
```bash
|
||
apt-get update -y
|
||
apt-get install -y ca-certificates curl gnupg curl git
|
||
|
||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||
apt-get install -y nodejs
|
||
|
||
node --version # >= 22
|
||
npm --version
|
||
```
|
||
|
||
## 2. Пользователь и каталоги
|
||
|
||
```bash
|
||
useradd --create-home --shell /bin/bash deploy
|
||
mkdir -p /opt/craftshop/server/uploads /opt/craftshop/www
|
||
chown -R deploy:deploy /opt/craftshop
|
||
chmod 755 /opt/craftshop /opt/craftshop/server /opt/craftshop/www
|
||
```
|
||
|
||
## 3. systemd unit
|
||
|
||
```bash
|
||
cat >/etc/systemd/system/craftshop-api.service <<'UNIT'
|
||
[Unit]
|
||
Description=Craftshop API (Fastify)
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=deploy
|
||
Group=deploy
|
||
WorkingDirectory=/opt/craftshop/server
|
||
EnvironmentFile=-/opt/craftshop/server/.env
|
||
ExecStart=/usr/bin/node src/index.js
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
LimitNOFILE=65535
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNIT
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable craftshop-api.service
|
||
```
|
||
|
||
## 4. Nginx
|
||
|
||
```bash
|
||
apt-get install -y nginx
|
||
|
||
cat >/etc/nginx/sites-available/craftshop <<'NGX'
|
||
server {
|
||
listen 80 default_server;
|
||
listen [::]:80 default_server;
|
||
|
||
server_name _;
|
||
|
||
root /opt/craftshop/www;
|
||
index index.html;
|
||
|
||
location /api/ {
|
||
client_max_body_size 250m;
|
||
proxy_pass http://127.0.0.1:3333;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
|
||
location /uploads/ {
|
||
proxy_pass http://127.0.0.1:3333;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
}
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
}
|
||
NGX
|
||
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
ln -sf /etc/nginx/sites-available/craftshop /etc/nginx/sites-enabled/craftshop
|
||
nginx -t && systemctl reload nginx
|
||
```
|
||
|
||
## 5. NetBird VPN
|
||
|
||
Установи NetBird и подключись к сети:
|
||
|
||
```bash
|
||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||
netbird up # потребуется SSO-логин
|
||
```
|
||
|
||
Проверь: `ip a` — должен появиться интерфейс `wt0` с IP из твоей NetBird-сети.
|
||
|
||
## 6. VPS с Nginx Proxy Manager
|
||
|
||
На VPS (где установлен NPM):
|
||
|
||
1. Добавь DNS-запись (A) для домена, указывающую на IP VPS
|
||
2. В NPM → Proxy Hosts → Add:
|
||
- Domain: `craftshop.твой-домен`
|
||
- Forward Hostname: `<NetBird-IP-LXC>` (IP интерфейса `wt0` на LXC)
|
||
- Forward Port: `80`
|
||
- SSL: запросить Let's Encrypt сертификат
|
||
3. Сохрани
|
||
|
||
## 7. Переменные окружения
|
||
|
||
```bash
|
||
cat >/opt/craftshop/server/.env <<'ENV'
|
||
DATABASE_URL="file:./prod.db"
|
||
PORT=3333
|
||
JWT_SECRET=<сгенерируй случайную строку>
|
||
ADMIN_EMAIL=<твой email>
|
||
CORS_ORIGIN=https://craftshop.твой-домен
|
||
IS_DEFAULT_CODE_ENABLED=false
|
||
ENV
|
||
chown deploy:deploy /opt/craftshop/server/.env
|
||
chmod 600 /opt/craftshop/server/.env
|
||
```
|
||
|
||
Сгенерировать `JWT_SECRET`:
|
||
|
||
```bash
|
||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||
```
|
||
|
||
## 8. Первый деплой
|
||
|
||
На машине разработчика:
|
||
|
||
```bash
|
||
./scripts/deploy-auto.sh --force
|
||
```
|
||
|
||
После деплоя:
|
||
|
||
```bash
|
||
systemctl start craftshop-api
|
||
systemctl status craftshop-api
|
||
curl http://127.0.0.1:3333/health
|
||
```
|
||
|
||
Также проверь через внешний URL: `https://craftshop.твой-домен/api/health`.
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add scripts/SERVER_SETUP.md && git commit -m "docs: add SERVER_SETUP.md for first-time LXC setup"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Update README.md
|
||
|
||
**Files:**
|
||
- Modify: `README.md`
|
||
|
||
- [ ] **Step 1: Remove old doc references and add deploy section**
|
||
|
||
Удалить строки со ссылками на удалённые doc-файлы:
|
||
- `docs/test-deploy-proxmox.md`
|
||
- `docs/deploy-changes.md`
|
||
- `docs/nginx-upload-limit.md`
|
||
|
||
Добавить в конец README.md:
|
||
|
||
```markdown
|
||
## Деплой
|
||
|
||
```bash
|
||
# Настроить scripts/deploy.env
|
||
cp scripts/deploy.env.example scripts/deploy.env
|
||
# Заполнить DEPLOY_HOST, DEPLOY_PATH
|
||
|
||
# Первичная настройка LXC: см. scripts/SERVER_SETUP.md
|
||
# Деплой только изменившихся компонентов:
|
||
./scripts/deploy-auto.sh
|
||
|
||
# Полный деплой (игнорировать diff):
|
||
./scripts/deploy-auto.sh --force
|
||
```
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add README.md && git commit -m "docs: update README with new deploy instructions"
|
||
```
|
||
|
||
---
|
||
|
||
## Self-review checklist
|
||
|
||
1. **Spec coverage:**
|
||
- Удаление файлов → Task 1 ✓
|
||
- .gitignore → Task 2 ✓
|
||
- deploy-auto.sh с детектом diff → Task 3 ✓
|
||
- Интерфейс команд (--force, -f, -b) → Task 3 ✓
|
||
- Transport rsync/tar → Task 3 ✓
|
||
- SERVER_SETUP.md → Task 4 ✓
|
||
- README.md → Task 5 ✓
|
||
- Все пункты spec покрыты ✓
|
||
|
||
2. **Placeholder scan:** полный код в каждом шаге, нет "TBD", "TODO", "implement later" ✓
|
||
3. **Type consistency:** единый подход к переменным (DEPLOY_*), флагам, transport-функциям ✓
|