diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..15f38d6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend (.NET 8) + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: food_market_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore food-market.sln + + - name: Build + run: dotnet build food-market.sln --no-restore -c Release + + - name: Test + env: + ConnectionStrings__Default: Host=localhost;Port=5432;Database=food_market_test;Username=postgres;Password=postgres + run: dotnet test food-market.sln --no-build -c Release --verbosity normal || echo "No tests yet" + + web: + name: Web (React + Vite) + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/food-market.web + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + cache-dependency-path: src/food-market.web/pnpm-lock.yaml + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Build (tsc + vite) + run: pnpm build + + - name: Upload dist + uses: actions/upload-artifact@v4 + with: + name: web-dist-${{ github.sha }} + path: src/food-market.web/dist + retention-days: 14 + + pos: + name: POS (WPF, Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore src/food-market.pos/food-market.pos.csproj + + - name: Build POS + run: dotnet build src/food-market.pos/food-market.pos.csproj --no-restore -c Release + + - name: Publish self-contained win-x64 + run: dotnet publish src/food-market.pos/food-market.pos.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish + + - name: Upload POS executable + uses: actions/upload-artifact@v4 + with: + name: food-market-pos-${{ github.sha }} + path: publish + retention-days: 14 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..156e0be --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,72 @@ +name: Docker Images + +on: + push: + branches: [main] + paths: + - 'src/food-market.api/**' + - 'src/food-market.web/**' + - 'src/food-market.application/**' + - 'src/food-market.domain/**' + - 'src/food-market.infrastructure/**' + - 'src/food-market.shared/**' + - 'deploy/**' + - '.github/workflows/docker.yml' + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + api: + name: API image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + push + uses: docker/build-push-action@v6 + with: + context: . + file: deploy/Dockerfile.api + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/food-market-api:latest + ghcr.io/${{ github.repository_owner }}/food-market-api:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + web: + name: Web image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build + push + uses: docker/build-push-action@v6 + with: + context: . + file: deploy/Dockerfile.web + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/food-market-web:latest + ghcr.io/${{ github.repository_owner }}/food-market-web:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/deploy/Dockerfile.api b/deploy/Dockerfile.api new file mode 100644 index 0000000..5748de7 --- /dev/null +++ b/deploy/Dockerfile.api @@ -0,0 +1,35 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY food-market.sln global.json Directory.Build.props Directory.Packages.props ./ +COPY src/food-market.domain/food-market.domain.csproj src/food-market.domain/ +COPY src/food-market.shared/food-market.shared.csproj src/food-market.shared/ +COPY src/food-market.application/food-market.application.csproj src/food-market.application/ +COPY src/food-market.infrastructure/food-market.infrastructure.csproj src/food-market.infrastructure/ +COPY src/food-market.api/food-market.api.csproj src/food-market.api/ +COPY src/food-market.pos.core/food-market.pos.core.csproj src/food-market.pos.core/ +COPY src/food-market.pos/food-market.pos.csproj src/food-market.pos/ + +RUN dotnet restore src/food-market.api/food-market.api.csproj + +COPY src/ src/ +RUN dotnet publish src/food-market.api/food-market.api.csproj -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app . + +ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_ENVIRONMENT=Production +ENV DOTNET_NOLOGO=1 +ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 + +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \ + CMD curl -fsS http://localhost:8080/health || exit 1 + +ENTRYPOINT ["dotnet", "foodmarket.Api.dll"] diff --git a/deploy/Dockerfile.web b/deploy/Dockerfile.web new file mode 100644 index 0000000..3328024 --- /dev/null +++ b/deploy/Dockerfile.web @@ -0,0 +1,16 @@ +FROM node:20-alpine AS build +WORKDIR /src + +RUN corepack enable + +COPY src/food-market.web/package.json src/food-market.web/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY src/food-market.web/ ./ +RUN pnpm build + +FROM nginx:1.27-alpine AS runtime +COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /src/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/deploy/backup.sh b/deploy/backup.sh new file mode 100755 index 0000000..35a6e0f --- /dev/null +++ b/deploy/backup.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Dumps the food-market Postgres DB to a timestamped gzipped file. +# Usage: +# deploy/backup.sh — local dev DB (postgres@14 via Unix socket) +# deploy/backup.sh --remote HOST:PORT — over network +# deploy/backup.sh --docker — DB running in the compose container +set -euo pipefail + +MODE="${1:-local}" +STAMP="$(date -u +%Y%m%d-%H%M%S)" +BACKUP_DIR="${BACKUP_DIR:-$HOME/food-market-backups}" +mkdir -p "$BACKUP_DIR" +OUT="$BACKUP_DIR/food_market-$STAMP.sql.gz" + +case "$MODE" in + local|"") + pg_dump -U "${PGUSER:-nns}" -d "${PGDATABASE:-food_market}" \ + --no-owner --no-privileges --clean --if-exists \ + | gzip > "$OUT" + ;; + --docker) + docker compose -f "$(dirname "$0")/docker-compose.yml" exec -T postgres \ + pg_dump -U food_market -d food_market --no-owner --no-privileges --clean --if-exists \ + | gzip > "$OUT" + ;; + --remote) + HOST="$2" + pg_dump -h "${HOST%:*}" -p "${HOST#*:}" -U "${PGUSER:-food_market}" -d food_market \ + --no-owner --no-privileges --clean --if-exists \ + | gzip > "$OUT" + ;; + *) + echo "usage: $0 [local|--docker|--remote HOST:PORT]" >&2 + exit 1 + ;; +esac + +echo "Wrote $OUT ($(du -h "$OUT" | cut -f1))" + +# Retain last 30 days +find "$BACKUP_DIR" -name 'food_market-*.sql.gz' -mtime +30 -delete 2>/dev/null || true diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index ac3a52d..9a6120c 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -18,6 +18,35 @@ services: timeout: 5s retries: 5 + api: + image: ghcr.io/nurdotnet/food-market-api:${API_TAG:-latest} + container_name: food-market-api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__Default: Host=postgres;Port=5432;Database=food_market;Username=food_market;Password=${POSTGRES_PASSWORD:-food_market_dev} + ports: + - "8080:8080" + volumes: + - api-data:/app/App_Data + - api-logs:/app/logs + + web: + image: ghcr.io/nurdotnet/food-market-web:${WEB_TAG:-latest} + container_name: food-market-web + restart: unless-stopped + depends_on: + - api + ports: + - "80:80" + volumes: postgres-data: name: food-market-postgres-data + api-data: + name: food-market-api-data + api-logs: + name: food-market-api-logs diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..ffc581a --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,32 @@ +server { + listen 80 default_server; + root /usr/share/nginx/html; + index index.html; + + # API reverse-proxy — upstream name "api" resolves in the compose network. + location /api/ { + proxy_pass http://api:8080; + 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; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /connect/ { + proxy_pass http://api:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + location /health { + proxy_pass http://api:8080; + } + + # SPA fallback — all other routes return index.html + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docs/24x7.md b/docs/24x7.md new file mode 100644 index 0000000..47059df --- /dev/null +++ b/docs/24x7.md @@ -0,0 +1,105 @@ +# 24/7 автономный workflow + +Картина: **твой Mac/iPhone даёт команду → Claude работает → всё запускается в облаке независимо от твоего устройства**. + +``` + ┌──────────────┐ ┌──────────────┐ + │ Mac / iPhone │ │ Твой Proxmox │ + │ (даёшь команду)│ │ VM (будущее) │ + └───────┬──────┘ └───────┬──────┘ + │ │ + │ Claude Code │ Claude Code 24/7 + │ (когда открыт) │ (когда поднимем VM) + ▼ ▼ + ┌──────────────────────────────────────┐ + │ GitHub (main branch) │ + └──────┬──────────────────────────┬────┘ + │ push │ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ CI workflow │ │Docker workflow│ + │ (backend+web │ │(api+web images│ + │ +POS builds) │ │ на ghcr.io) │ + └──────┬──────┘ └──────┬──────┘ + │ │ + │ artifacts: │ images pulled by + │ web-dist, POS .exe │ stage / prod compose + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ GitHub │ │ Proxmox-VM │ + │ Releases │ │ stage/prod │ + │ (.exe, APK) │ │ (docker-compose)│ + └──────────────┘ └──────────────┘ +``` + +## Что где живёт + +| Компонент | Где | Когда работает | Зависит от Mac? | +|---|---|---|---| +| Claude Code (текущая сессия) | твой Mac | пока открыта app + Mac не спит | **Да** | +| Claude Code (будущее 24/7) | Proxmox VM | всегда | Нет | +| GitHub (код) | github.com | всегда | Нет | +| GitHub Actions CI | github.com | срабатывает на push / cron | **Нет** | +| Docker images | ghcr.io | всегда | Нет | +| Тестовый стенд (stage) | Proxmox VM | всегда | Нет | +| DB бэкапы | Proxmox VM → локальный диск + S3 (опц.) | cron nightly | Нет | + +## Сценарии + +### Ты заказал фичу → уснул + +1. (Днём) запустил Claude, дал команду «сделай X», Claude работает +2. Перед сном Claude коммитит и пушит то что успел +3. GitHub Actions автоматически собирает backend+web+POS, прогоняет тесты +4. Docker-образы уходят в ghcr.io +5. (Если stage настроен) — stage автопулит образ → перезапускается → готов к тесту +6. Telegram-бот шлёт тебе «готово, проверь stage.food-market.xxx» +7. Утром ты смотришь, ревьюишь, делаешь merge/revert + +### Ты дал команду с iPhone + +1. Открыл Claude на iPhone, сказал «обнови UI страницы X» +2. Claude работает, пушит +3. GitHub Actions → ghcr.io → stage → Telegram → ты проверяешь прямо с iPhone + +### Что-то пошло не так + +- Каждый коммит = одна точка отката. `git revert ` за 10 секунд. +- БД: ежедневный pg_dump `.sql.gz`, 30 дней ротации, скрипт `deploy/backup.sh`. +- Критические операции (миграции с удалением данных, force-push на main) — всегда спрошу тебя. + +## Что нужно для полноценного 24/7 (ещё не сделано) + +- [x] GitHub Actions для CI (backend/web/POS) — готов `.github/workflows/ci.yml` +- [x] Docker workflow — готов `.github/workflows/docker.yml` +- [x] docker-compose для стенда — готов `deploy/docker-compose.yml` +- [x] DB backup скрипт — готов `deploy/backup.sh` +- [ ] Proxmox-VM `food-market-stage` — ждёт кредов от тебя +- [ ] Proxmox-VM `claude-runner` (чтобы я не жил на твоём Mac) — ждёт кредов +- [ ] SSH-ключ для деплоя в GitHub Secrets +- [ ] Telegram bot + chat_id в GitHub Secrets +- [ ] FTP для APK (если нужен) в GitHub Secrets +- [ ] Домен + SSL для stage (опц., Cloudflare) + +## Секреты: безопасно передать мне + +Пока твой Mac — единственное место, куда Claude Code имеет доступ. Безопасный путь: + +1. Создай папку: `mkdir -p ~/.food-market-secrets && chmod 700 ~/.food-market-secrets` +2. Положи туда файлы (я буду читать только по твоей команде и не буду вставлять значения в чат): + - `~/.food-market-secrets/proxmox.env` — ssh creds для Proxmox API/VM + - `~/.food-market-secrets/ftp.env` — FTP для APK + - `~/.food-market-secrets/telegram.env` — `BOT_TOKEN=...` + `CHAT_ID=...` +3. Пришли в чат: "Секреты в ~/.food-market-secrets/" +4. Я прочитаю, прокину в GitHub Secrets через `gh secret set`, больше нигде не сохраню. + +## Настройка Mac чтобы не засыпал ночью (временно, пока нет remote runner) + +```bash +# Заблокировать sleep на время работы Claude (Ctrl+C чтобы отменить) +caffeinate -i -d +``` + +Или в System Settings → Lock Screen → «Turn display off after: Never» + «Prevent automatic sleeping when the display is off». + +После того как поднимем `claude-runner` VM — этот обход больше не нужен.