diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index f27f37e..0000000 --- a/.drone.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Drone CI/CD бэкенда (сервер подключён к Gitea; см. README, «CI/CD»). -# -# Push/PR — только проверки и сборка образа. Деплой — ТОЛЬКО ручным -# запуском пайплайна из UI Drone (событие custom, кнопка Run Pipeline): -# шаг deploy выполняется исключительно при нём. -kind: pipeline -type: docker -name: ci-cd - -clone: - depth: 1 - -# Сервисная база для интеграционных тестов: схему накатывает сам шаг -# test (cmd/migrate), тесты выполняют сценарии в транзакции и откатывают -# их — данные в базе не остаются. -services: - - name: postgres - image: postgres:16-alpine - environment: - POSTGRES_USER: building - POSTGRES_PASSWORD: building - POSTGRES_DB: building_mgmt_test - -steps: - - name: test - # Полный golang-образ, не alpine: -race требует CGO и C-компилятора. - image: golang:1.27 - environment: - DATABASE_URL: postgres://building:building@postgres:5432/building_mgmt_test?sslmode=disable - commands: - - go vet ./... - # Postgres в сервисе поднимается параллельно со сборкой шага: - # повторяем накат, пока база не готова (30 попыток по 2 секунды). - - ok=0; for i in $(seq 1 30); do if go run ./cmd/migrate; then ok=1; break; fi; sleep 2; done; test "$ok" = 1 - - go test -race -count=1 ./... - - go build ./... - - - name: lint - image: ghcr.io/golangci/golangci-lint:v2.14.0 - commands: - - golangci-lint run ./... - - - name: build-api - # Образ и два тега: latest (его поднимает compose.prod.yaml) и SHA - # (для отката: собрать нужный коммит и перетегнуть latest вручную). - image: plugins/docker:27 - settings: - repo: building-mgmt-api - tags: - - latest - - ${DRONE_COMMIT_SHA} - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - - name: deploy - image: docker:27-cli - volumes: - - /var/run/docker.sock:/var/run/docker.sock - commands: - # -p закрепляет имя проекта: у Drone каталог клонирования свой, - # без -p имя сети стека разъехалось бы между запусками, и web - # перестал бы находить api. env-файл — постоянный файл на сервере. - - docker compose version - - docker compose -p building-mgmt --env-file /opt/building-mgmt/.env -f compose.prod.yaml up -d --wait - when: - event: - - custom diff --git a/.env.prod.example b/.env.prod.example index 8eeda8e..d529e11 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -3,7 +3,7 @@ # mkdir -p /opt/building-mgmt && cp .env.prod.example /opt/building-mgmt/.env # chmod 600 /opt/building-mgmt/.env # секреты, только root # -# Заполняется один раз и правится осознанно: стек и Drone читают его +# Заполняется один раз и правится осознанно: стек и CI читают его # по пути /opt/building-mgmt/.env. Пароли и секреты в git не входят. # --- Обязательные переменные (compose упадёт без них: :? фейл-фаст) --- diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..fe62699 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,90 @@ +# CI/CD бэкенда через Gitea Actions (встроенный в Gitea, раннер на сервере). +# +# push/PR: go vet → накат миграций в сервисную postgres → go test -race → +# go build; параллельно golangci-lint; затем сборка прод-образа api +# (локальные теги latest и короткий SHA, реестр не используется). +# Деплой — ТОЛЬКО ручной запуск workflow (кнопка Run workflow): compose-файлы +# берутся из дерева этого же коммита (actions/checkout), секреты не нужны. +# web перезапускает свой workflow в репо веба — репозитории независимы. +# +# Требования к раннеру (см. README, раздел «CI/CD (Gitea Actions)»): +# runner.labels: [docker] — совпадает с runs-on ниже; +# container.valid_volumes: /var/run/docker.sock, /opt/building-mgmt — +# сокет docker для сборки/деплоя и каталог стека с .env. +name: backend + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: docker + container: golang:1.27 + # Сервисная база для интеграционных тестов: схему накатывает шаг + # test (cmd/migrate), тесты выполняют сценарии в транзакции и + # откатывают их — данные в базе не остаются. + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: building + POSTGRES_PASSWORD: building + POSTGRES_DB: building_mgmt_test + env: + DATABASE_URL: postgres://building:building@postgres:5432/building_mgmt_test?sslmode=disable + steps: + - uses: actions/checkout@v4 + - name: go vet + run: go vet ./... + - name: Накат миграций (ждём сервисную postgres) + # Postgres поднимается параллельно с контейнером джобы: + # повторяем накат, пока база не готова (30 попыток по 2 секунды). + run: | + ok=0; for i in $(seq 1 30); do if go run ./cmd/migrate; then ok=1; break; fi; sleep 2; done; test "$ok" = 1 + - name: Тесты + # -race требует CGO — полный golang-образ, не alpine. + run: go test -race -count=1 ./... + - name: Сборка + run: go build ./... + + lint: + runs-on: docker + container: ghcr.io/golangci/golangci-lint:v2.14.0 + steps: + - uses: actions/checkout@v4 + - name: golangci-lint + run: golangci-lint run ./... + + build-api: + runs-on: docker + container: docker:27-cli + steps: + - uses: actions/checkout@v4 + - name: Сборка прод-образа api + # Образ и два тега: latest (его поднимает compose.prod.yaml) и + # короткий SHA (для отката: перетегнуть latest вручную). + run: | + short="$(echo "$GITHUB_SHA" | cut -c1-7)" + docker build -t building-mgmt-api:latest -t "building-mgmt-api:${short}" . + + deploy: + if: github.event_name == 'workflow_dispatch' + needs: build-api + runs-on: docker + container: docker:27-cli + steps: + - uses: actions/checkout@v4 + - name: Подъём db/minio/api стека + # compose-CLI ставим в джоб-контейнер; docker — через смонтированный + # сокет хоста (valid_volumes у раннера). -p закрепляет имя проекта: + # оно задаёт имя сети, и web из своего workflow находит api. + # web не трогаем — его перезапускает workflow репо веба. + run: | + apk add --no-cache docker-cli-compose + docker compose -p building-mgmt \ + --env-file /opt/building-mgmt/.env \ + -f compose.prod.yaml \ + up -d --wait db minio api diff --git a/README.md b/README.md index 9b5c6f3..7d21d8a 100644 --- a/README.md +++ b/README.md @@ -892,13 +892,13 @@ Handlers не отдают тексты ошибок БД наружу — он golangci-lint run ./... # или: make lint ``` -- **CI** — Drone (`.drone.yml`, сервер подключён к Gitea): `vet` → накат - миграций на сервисный PostgreSQL → `go test -race ./...` → `go build` → - `golangci-lint` → сборка docker-образа. Тесты в CI идут **на живой - базе**, а не скипаются: без `DATABASE_URL` интеграционные тесты - пропускаются молча. `-race` на живой базе дороже, зато ловит гонки - между пулом соединений, лимитером и горутинами хендлеров. Деплой в - прод — ручным запуском того же пайплайна (см. «CI/CD (Drone + Gitea)»). +- **CI** — Gitea Actions (`.gitea/workflows/ci.yml`, встроенный в Gitea): + `vet` → накат миграций на сервисный PostgreSQL → `go test -race ./...` → + `go build` → `golangci-lint` → сборка docker-образа. Тесты в CI идут + **на живой базе**, а не скипаются: без `DATABASE_URL` интеграционные + тесты пропускаются молча. `-race` на живой базе дороже, зато ловит + гонки между пулом соединений, лимитером и горутинами хендлеров. Деплой + в прод — ручным запуском того же workflow (см. «CI/CD (Gitea Actions)»). - **Команды** — `Makefile`: `run`, `build`, `test`, `race`, `vet`, `lint`, `fmt`, `swag`, `migrate`, `seed`, `up`, `down`, `tidy`. На Windows без @@ -1028,7 +1028,7 @@ cp /opt/building-mgmt-repo/.env.prod.example /opt/building-mgmt/.env chmod 600 /opt/building-mgmt/.env $EDITOR /opt/building-mgmt/.env # пароли, секреты, домен — см. комментарии -# 2. Образы (Drone собирает их сам; вручную — если CI ещё не подключён) +# 2. Образы (CI собирает их сам; вручную — если CI ещё не подключён) docker build -t building-mgmt-api:latest /opt/building-mgmt-repo docker build -t building-mgmt-web:latest <клон репо web> @@ -1038,7 +1038,7 @@ docker compose -p building-mgmt \ -f /opt/building-mgmt-repo/compose.prod.yaml up -d --wait ``` -`-p building-mgmt` обязателен и в ручных запусках, и в Drone: имя +`-p building-mgmt` обязателен и в ручных запусках, и в CI: имя проекта задаёт имя сети, разные имена разнесут web и api по разным сетям, и прокси перестанет находить api. Проверка: `http://<домен>/ping` → `{"message":"pong"}`. `APP_ENV=production` (задан в шаблоне) блокирует @@ -1076,50 +1076,59 @@ docker run --rm -v building-mgmt_miniodata:/data -v "$PWD":/backup alpine \ --- -## CI/CD (Drone + Gitea) +## CI/CD (Gitea Actions) -CI живёт в Drone: сервер подключён к Gitea, пайплайны — `.drone.yml` в -обоих репозиториях (бэкенд и веб). GitHub Actions больше нет. +CI/CD живёт во встроенном Gitea Actions: пайплайны — +`.gitea/workflows/ci.yml` в обоих репозиториях (бэкенд и веб), сервер — +тот же `axe.engir.by`. GitHub Actions и Drone не используются. ### Установка (однократно, на прод-сервере) -```bash -# В Gitea: Настройки → Applications → OAuth2-приложение, -# Redirect URI: http://<хост-drone>:8081/authorize -mkdir -p /opt/drone && cp deploy/drone/compose.yaml /opt/drone/ -# /opt/drone/.env: GITEA_SERVER, DRONE_GITEA_CLIENT_ID, -# DRONE_GITEA_CLIENT_SECRET, DRONE_GITEA_ADMIN_USERNAME, -# DRONE_RPC_SECRET= # openssl rand -hex 32 -docker compose -p drone --env-file /opt/drone/.env \ - -f /opt/drone/compose.yaml up -d +```ini +# app.ini Gitea (Gitea ≥ 1.21), затем перезапуск Gitea: +[actions] +ENABLED = true ``` -Drone UI — `http://<хост>:8081` (за TLS-терминалом); оттуда же Gitea -шлёт webhook. Runner исполняет шаги пайплайнов в docker на этом же -сервере, одна задача за раз (`capacity=1` — деплои не пересекаются). +Раннер исполняет задачи в docker на этом же сервере. Регистрация — +токеном из UI Gitea (Администрирование → Раннеры → Создать +регистрационный токен, метка `docker`), каталог `/opt/act-runner`; в +`config.yaml` раннера обязательны две вещи: -### Секреты пайплайна (UI Drone, репозиторий веба) +```yaml +runner: + labels: [docker] # совпадает с runs-on: docker в workflow +container: + valid_volumes: # сокет docker для сборки/деплоя образов и + - /var/run/docker.sock # каталог стека с постоянным .env + - /opt/building-mgmt +``` -- `gitea_host`, `gitea_user`, `gitea_token` — доступ к приватному репо - бэкенда: web-пайплайн клонирует его за токен, чтобы перезапустить - `web` тем же `compose.prod.yaml` (один стек, один проект). +### Секреты (Настройки репо → Секреты → Actions) -Секреты стека (`/opt/building-mgmt/.env`) в Drone не хранятся: пайплайн +- бэкенд (`axe`) — секреты не нужны: деплой берёт `compose.prod.yaml` + из дерева того же коммита (`actions/checkout`); +- веб (`axe-web`): `CI_HOST` (`axe.engir.by`), `CI_USER` (`edadmin`), + `CI_TOKEN` (PAT с правом `write:repository`) — web-workflow скачивает + тарбол репо бэкенда, чтобы перезапустить `web` тем же + `compose.prod.yaml` (один стек, один проект). Имена с префиксом + `GITEA_` Gitea запрещает — префикс зарезервирован. + +Секреты стека (`/opt/building-mgmt/.env`) в Actions не хранятся: деплой только читает файл на сервере флагом `--env-file`. -### Что делает каждый пайплайн +### Что делает каждый workflow - бэкенд (push/PR и ручной запуск): `vet` → `migrate` → `test -race` → `go build` → `golangci-lint` → сборка образа `building-mgmt-api:latest` + тег по SHA; - веб (push/PR и ручной запуск): `node --check` js → сборка образа `building-mgmt-web:latest` + тег по SHA; -- **деплой — только ручным запуском** (кнопка **Run Pipeline** в UI, - событие `custom`): бэкенд — `docker compose -p building-mgmt - --env-file /opt/building-mgmt/.env -f compose.prod.yaml up -d --wait` - (пересоздаёт api новым образом, данные не трогает); веб — то же - с `--no-deps web`. Push в `main` образы собирает, но на сервер ничего - не ставит. +- **деплой — только ручным запуском** (кнопка **Run workflow**): бэкенд — + `docker compose -p building-mgmt --env-file /opt/building-mgmt/.env + -f compose.prod.yaml up -d --wait db minio api` (пересоздаёт api новым + образом, данные не трогает); веб — то же с `--no-deps web`. Push в + `main` образы собирает, но на сервер ничего не ставит. --- diff --git a/compose.prod.yaml b/compose.prod.yaml index 0f39b79..ae74a92 100644 --- a/compose.prod.yaml +++ b/compose.prod.yaml @@ -2,7 +2,7 @@ # web (порт 80). Postgres и MinIO портов наружу не пробрасывают вовсе — # они видны только контейнерам приложения. # -# Образы api и web заранее собраны Drone (или вручную): +# Образы api и web заранее собраны CI (или вручную): # docker build -t building-mgmt-api:latest . # в этом репо # docker build -t building-mgmt-web:latest . # в репо web # @@ -11,7 +11,7 @@ # --env-file /opt/building-mgmt/.env \ # -f compose.prod.yaml up -d --wait # -# Имя проекта (-p) закреплено: по нему считается имя сети. Drone и +# Имя проекта (-p) закреплено: по нему считается имя сети. CI и # ручной запуск обязаны давать одно и то же имя, иначе web не найдёт # api и стек разъедется на два. # @@ -71,7 +71,7 @@ services: api: image: building-mgmt-api:latest - # build: — на случай ручной сборки без Drone: если образа нет, + # build: — на случай ручной сборки без CI: если образа нет, # compose соберёт его из этого же репо; готовый образ не трогает. build: context: . diff --git a/deploy/drone/compose.yaml b/deploy/drone/compose.yaml deleted file mode 100644 index 497f20c..0000000 --- a/deploy/drone/compose.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Drone CI: server (интерфейс, webhook от Gitea) и docker-runner -# (исполняет шаги .drone.yml в контейнерах этого хоста). -# -# Перед запуском — в Gitea: Настройки → Applications → OAuth2-приложение, -# Redirect URI: http://<хост-drone>:8081/authorize -# -# .env рядом с этим файлом (в git не входит): -# GITEA_SERVER=https://gitea.example.com -# DRONE_GITEA_CLIENT_ID=... -# DRONE_GITEA_CLIENT_SECRET=... -# DRONE_GITEA_ADMIN_USERNAME=admin # аккаунт Gitea, станет админом Drone -# DRONE_SERVER_HOST=drone.example.com -# DRONE_SERVER_PROTO=https -# DRONE_RPC_SECRET= # openssl rand -hex 32 -# -# Запуск: docker compose -p drone --env-file .env -f compose.yaml up -d - -services: - drone: - image: drone/drone:2 - restart: unless-stopped - ports: - # Веб-интерфейс и цель webhook от Gitea; за TLS-терминалом. - - "8081:80" - environment: - DRONE_GITEA_SERVER: ${GITEA_SERVER:?GITEA_SERVER is required} - DRONE_GITEA_CLIENT_ID: ${DRONE_GITEA_CLIENT_ID:?DRONE_GITEA_CLIENT_ID is required} - DRONE_GITEA_CLIENT_SECRET: ${DRONE_GITEA_CLIENT_SECRET:?DRONE_GITEA_CLIENT_SECRET is required} - # Общий секрет server ↔ runner: openssl rand -hex 32. - DRONE_RPC_SECRET: ${DRONE_RPC_SECRET:?DRONE_RPC_SECRET is required} - # Первый залогинившийся через Gitea становится админом Drone. - DRONE_USER_CREATE_OAUTH: username=${DRONE_GITEA_ADMIN_USERNAME:?DRONE_GITEA_ADMIN_USERNAME is required},admin=true - # Внешний адрес интерфейса: его видят Gitea (webhook) и браузер. - DRONE_SERVER_HOST: ${DRONE_SERVER_HOST:?DRONE_SERVER_HOST is required} - DRONE_SERVER_PROTO: ${DRONE_SERVER_PROTO:-https} - volumes: - - drone:/data - networks: - - drone - - runner: - image: drone/drone-runner-docker:1 - restart: unless-stopped - depends_on: - - drone - environment: - DRONE_RPC_PROTO: http - DRONE_RPC_HOST: drone - DRONE_RPC_SECRET: ${DRONE_RPC_SECRET:?DRONE_RPC_SECRET is required} - # Один конвейер за раз: деплои не должны пересекаться. - DRONE_RUNNER_CAPACITY: "1" - DRONE_RUNNER_NAME: prod-server - # Шаги пайплайнов (и их сервисы — postgres для тестов) поднимаются - # именно в этой сети: имя сети указано явно (name: drone), чтобы - # совпадало со значением DRONE_RUNNER_NETWORKS. - DRONE_RUNNER_NETWORKS: drone - volumes: - # Доступ к docker-демону: сборка образов, compose, деплой. - - /var/run/docker.sock:/var/run/docker.sock - networks: - - drone - -networks: - # Имя фиксируем: DRONE_RUNNER_NETWORKS ссылается на него буквально, - # а префикс проекта (drone_drone) runner бы не узнал. - drone: - name: drone - -volumes: - drone: