Files
shop-server/docs/superpowers/plans/2026-05-14-auto-deploy.md
T

547 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-функциям ✓