8814fe7cf4
Версия — в одном месте: файл VERSION в корне, из него её берут Makefile, deploy/install.sh (IMAGE_TAG) и desktop-обёртка (tauri.conf.json, Cargo.toml). Публикация обновлений (scripts/desktop-release.sh): - манифесты прошлых релизов удаляются: оставшийся <старая версия>.json перекрывал новый релиз — обёртка спрашивает манифест своей версии и видела «обновлений нет»; - mktemp-каталог раскладки получает 755, каталоги на инстансе нормализуются: rsync -a переносил режим 0700 на корень каталога обновлений, и приложение не могло читать манифесты (404 при живых файлах); - --remote работает без TTY и без sudo, если каталог обновлений записываем; опции ssh — в GLCHAT_SSH_OPTS; - предупреждение при публикации версии старше уже опубликованной. Сервер: HEAD /updates/... (chi не выводит его из GET) — размер и тип артефакта без скачивания; тест TestUpdatesAnswersHead. Приёмка на живом канале: 0.1.0 → 0.1.1 на стенде (скачивание, проверка подписи, установка, перезапуск), /Applications/glchat.app = 0.1.1.
288 lines
13 KiB
Makefile
288 lines
13 KiB
Makefile
SHELL := /usr/bin/env bash
|
||
.DEFAULT_GOAL := help
|
||
|
||
APP := glchat
|
||
MODULE := glchat
|
||
# Версия релиза — единственный источник правды: файл VERSION в корне (его же
|
||
# читает установщик, а tauri.conf.json/Cargo.toml держат то же значение).
|
||
# Без файла (например, в архиве без него) работает прежнее правило по git.
|
||
VERSION ?= $(shell cat $(CURDIR)/VERSION 2>/dev/null || git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
|
||
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
|
||
ROOT := $(CURDIR)
|
||
CACHE_DIR := $(ROOT)/.cache
|
||
BIN_DIR := $(CACHE_DIR)/bin
|
||
BUILD_DIR := $(ROOT)/build
|
||
export GOCACHE := $(CACHE_DIR)/go-build
|
||
export GOMODCACHE:= $(CACHE_DIR)/go-mod
|
||
export GOPATH := $(CACHE_DIR)/gopath
|
||
export GOTOOLCHAIN := local
|
||
export GOLANGCI_LINT_CACHE := $(CACHE_DIR)/golangci-lint
|
||
# Сети без доступа к storage.googleapis.com (например, домашний сервер) не могут
|
||
# скачать модули через proxy.golang.org; goproxy.io работает как fallback.
|
||
export GOPROXY ?= https://proxy.golang.org,direct
|
||
export GOSUMDB ?= sum.golang.org
|
||
export CGO_ENABLED ?= 1
|
||
|
||
# sqlite_fts5 включает полнотекстовый поиск в mattn/go-sqlite3 (AGENT.md 7.15):
|
||
# без тега поиск по сообщениям не работает, поэтому он обязателен для всех сборок.
|
||
GO_TAGS ?= sqlite_fts5
|
||
GOFLAGS_LOCAL := -trimpath -tags $(GO_TAGS)
|
||
GO_LDFLAGS := -s -w \
|
||
-X main.version=$(VERSION) \
|
||
-X main.commit=$(COMMIT) \
|
||
-X main.buildDate=$(BUILD_DATE)
|
||
|
||
# Инструмент берётся из .cache/bin; если он уже установлен в системе
|
||
# (PATH), повторная установка не нужна — это важно для CI и серверов,
|
||
# где сборка инструментов из исходников недоступна.
|
||
SYSTEM_TOOLS := golangci-lint gofumpt sqlc goose govulncheck gitleaks
|
||
export PATH := $(BIN_DIR):$(PATH)
|
||
|
||
GO_PACKAGES := ./cmd/... ./internal/...
|
||
GO_LINT_TIMEOUT ?= 5m
|
||
|
||
WEB_DIR := $(ROOT)/web
|
||
DOCKER_IMAGE ?= glchat
|
||
DOCKER_TAG ?= $(VERSION)
|
||
SKIP_TLS ?= 0
|
||
|
||
.PHONY: help
|
||
help: ## Показать список целей
|
||
@printf 'glchat — доступные цели:\n\n'
|
||
@grep -hE '^[a-zA-Z0-9_.-]+:.*?## ' $(MAKEFILE_LIST) | \
|
||
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}'
|
||
@printf '\nПрофиль: %s | Кэш: %s\n' "$${PROFILE:-auto}" "$(CACHE_DIR)"
|
||
|
||
.PHONY: tools
|
||
tools: ## Установить dev-инструменты в .cache/bin (уже установленные в системе пропускаются)
|
||
@mkdir -p $(CACHE_DIR) $(BIN_DIR)
|
||
@for tool in $(SYSTEM_TOOLS); do \
|
||
if command -v $$tool >/dev/null 2>&1; then \
|
||
printf ' ok %-16s %s\n' "$$tool" "$$(command -v $$tool)"; \
|
||
else \
|
||
$(MAKE) --no-print-directory $(BIN_DIR)/$$tool || exit 1; \
|
||
fi; \
|
||
done
|
||
|
||
$(BIN_DIR)/golangci-lint: | $(CACHE_DIR)
|
||
@echo "==> install golangci-lint v2.13.2"
|
||
@GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2
|
||
|
||
$(BIN_DIR)/gofumpt: | $(CACHE_DIR)
|
||
@echo "==> install gofumpt"
|
||
@GOBIN=$(BIN_DIR) go install mvdan.cc/gofumpt@v0.12.0
|
||
|
||
$(BIN_DIR)/sqlc: | $(CACHE_DIR)
|
||
@echo "==> install sqlc"
|
||
@GOBIN=$(BIN_DIR) go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1
|
||
|
||
$(BIN_DIR)/goose: | $(CACHE_DIR)
|
||
@echo "==> install goose"
|
||
@GOBIN=$(BIN_DIR) go install github.com/pressly/goose/v3/cmd/goose@v3.28.0
|
||
|
||
$(BIN_DIR)/govulncheck: | $(CACHE_DIR)
|
||
@echo "==> install govulncheck"
|
||
@GOBIN=$(BIN_DIR) go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
|
||
|
||
$(BIN_DIR)/gitleaks: | $(CACHE_DIR)
|
||
@echo "==> install gitleaks"
|
||
@GOBIN=$(BIN_DIR) go install github.com/zricethezav/gitleaks/v8@v8.30.0
|
||
|
||
.PHONY: dev
|
||
dev: ## Запустить сервер локально (данные в ./build/dev-data)
|
||
@mkdir -p $(BUILD_DIR)/dev-data/db
|
||
DATA_DIR=$(BUILD_DIR)/dev-data \
|
||
LISTEN_ADDR=127.0.0.1:8080 \
|
||
DOMAIN=localhost \
|
||
LOG_FORMAT=text \
|
||
LOG_LEVEL=debug \
|
||
go run ./cmd/glchat
|
||
|
||
.PHONY: build
|
||
build: ## Собрать бинарник сервера в ./build/glchat
|
||
@mkdir -p $(BUILD_DIR)
|
||
go build $(GOFLAGS_LOCAL) -ldflags '$(GO_LDFLAGS)' -o $(BUILD_DIR)/$(APP) ./cmd/glchat
|
||
@ls -lh $(BUILD_DIR)/$(APP)
|
||
|
||
.PHONY: test
|
||
test: ## Тесты Go с race-детектором
|
||
go test -tags $(GO_TAGS) -race -count=1 $(GO_PACKAGES)
|
||
|
||
.PHONY: test-cover
|
||
test-cover: ## Тесты с отчётом покрытия (./build/coverage.out)
|
||
@mkdir -p $(BUILD_DIR)
|
||
go test -tags $(GO_TAGS) -race -count=1 -coverprofile=$(BUILD_DIR)/coverage.out $(GO_PACKAGES)
|
||
go tool cover -func=$(BUILD_DIR)/coverage.out | tail -1
|
||
|
||
.PHONY: lint
|
||
lint: tools go-lint web-lint ## Полный линт (Go + web)
|
||
|
||
.PHONY: go-lint
|
||
go-lint: tools ## Линт Go: формат, vet, golangci-lint
|
||
@echo "==> формат (gofumpt + golangci-lint fmt)"
|
||
@out=$$(gofumpt -l ./cmd ./internal ./scripts 2>/dev/null || true); \
|
||
if [ -n "$$out" ]; then printf '%s\n' "$$out"; echo "run: make fmt"; exit 1; fi
|
||
@out=$$(golangci-lint fmt --diff $(GO_PACKAGES)); \
|
||
if [ -n "$$out" ]; then printf '%s\n' "$$out"; echo "run: make fmt"; exit 1; fi
|
||
@echo "==> go vet"
|
||
@go vet $(GO_PACKAGES)
|
||
@echo "==> golangci-lint"
|
||
@golangci-lint run --timeout $(GO_LINT_TIMEOUT) $(GO_PACKAGES)
|
||
|
||
.PHONY: fmt
|
||
fmt: tools ## Форматировать Go-код
|
||
gofumpt -w ./cmd ./internal
|
||
golangci-lint fmt $(GO_PACKAGES)
|
||
|
||
.PHONY: generate
|
||
generate: tools ## Проверить сгенерированные артефакты (OpenAPI, sqlc — по мере появления)
|
||
@echo "==> openapi: обслуживается из internal/server/openapi.go (/api/v1/openapi.json)"
|
||
@test -s internal/server/openapi.go || { echo "openapi.go отсутствует"; exit 1; }
|
||
@if [ -d queries ] && [ -n "$$(ls -A queries 2>/dev/null | grep -v '^.gitkeep$$')" ]; then \
|
||
echo "==> sqlc generate"; sqlc generate; \
|
||
else echo "==> sqlc: запросов пока нет, пропускаем"; fi
|
||
|
||
.PHONY: security
|
||
security: tools ## AppSec: govulncheck, gitleaks, npm audit, trivy (по возможности)
|
||
@mkdir -p $(BUILD_DIR)
|
||
@echo "==> govulncheck"
|
||
@govulncheck ./cmd/... ./internal/... || { echo "govulncheck нашёл уязвимости"; exit 1; }
|
||
@echo "==> gitleaks"
|
||
@$(ROOT)/scripts/gitleaks-scan.sh
|
||
@echo "==> npm audit (web, только production-зависимости)"
|
||
@cd $(WEB_DIR) && npm audit --omit=dev --audit-level=high || { echo "npm audit нашёл уязвимости"; exit 1; }
|
||
@echo "==> trivy filesystem scan"
|
||
@if command -v trivy >/dev/null 2>&1; then \
|
||
trivy fs --quiet --scanners vuln,secret,misconfig --exit-code 1 \
|
||
--skip-dirs .cache --skip-dirs .git --skip-dirs web/node_modules .; \
|
||
else echo "trivy not installed — skipped"; fi
|
||
|
||
.PHONY: sbom
|
||
sbom: ## SBOM зависимостей в build/sbom.cdx.json (trivy, если установлен)
|
||
@mkdir -p $(BUILD_DIR)
|
||
@if command -v trivy >/dev/null 2>&1; then \
|
||
trivy fs --quiet --format cyclonedx --output $(BUILD_DIR)/sbom.cdx.json \
|
||
--skip-dirs .cache --skip-dirs web/node_modules . && \
|
||
echo "SBOM: $(BUILD_DIR)/sbom.cdx.json"; \
|
||
else echo "trivy не установлен — SBOM не собран (запустите на сервере сборки)"; fi
|
||
|
||
.PHONY: secrets-scan
|
||
secrets-scan: tools ## Только проверка на утечки секретов
|
||
@$(ROOT)/scripts/gitleaks-scan.sh
|
||
|
||
.PHONY: web-install
|
||
web-install: ## Установить зависимости web-клиента
|
||
cd $(WEB_DIR) && npm ci
|
||
|
||
.PHONY: web-lint
|
||
web-lint: ## Линт и типизация web-клиента
|
||
cd $(WEB_DIR) && npm run lint
|
||
cd $(WEB_DIR) && npm run check
|
||
|
||
.PHONY: web-test
|
||
web-test: ## Тесты web-клиента (vitest)
|
||
cd $(WEB_DIR) && npm test
|
||
|
||
.PHONY: web-build
|
||
web-build: ## Сборка web-клиента в web/dist
|
||
cd $(WEB_DIR) && npm run build
|
||
|
||
# Desktop-обёртка (Фаза 6, docs/client-tauri.md). Кэши инструментов — в .cache,
|
||
# адрес инстанса по умолчанию задаёт сама обёртка (https://gl.mhspx.su).
|
||
DESKTOP_DIR := $(ROOT)/desktop
|
||
DESKTOP_TAURI_DIR := $(DESKTOP_DIR)/src-tauri
|
||
DESKTOP_KEY ?= $(BUILD_DIR)/desktop-keys/glchat-updater.key
|
||
# Бандлы Tauri: `all` — обычная сборка (значение по умолчанию из tauri.conf.json),
|
||
# `app` — только .app (и артефакты обновления) без .dmg: сборка .dmg монтирует
|
||
# образ и требует Finder. Tauri CLI не принимает `all` в --bundles: при `all`
|
||
# флаг не передаётся вовсе, и список бандлов берётся из bundle.targets.
|
||
DESKTOP_BUNDLES ?= all
|
||
DESKTOP_BUNDLES_ARG := $(if $(filter-out all,$(DESKTOP_BUNDLES)),--bundles $(DESKTOP_BUNDLES),)
|
||
TAURI_CLI ?= npx --yes @tauri-apps/cli@^2
|
||
export CARGO_HOME ?= $(CACHE_DIR)/cargo
|
||
export NPM_CONFIG_CACHE ?= $(CACHE_DIR)/npm
|
||
|
||
.PHONY: desktop-check
|
||
desktop-check: ## Проверки Rust-обёртки: fmt, clippy, тесты
|
||
cd $(DESKTOP_TAURI_DIR) && cargo fmt --check
|
||
cd $(DESKTOP_TAURI_DIR) && cargo clippy --release --all-targets
|
||
cd $(DESKTOP_TAURI_DIR) && cargo test --release
|
||
|
||
.PHONY: desktop-build
|
||
desktop-build: ## Собрать desktop-приложение (бандлы по умолчанию для текущей ОС)
|
||
@if [ -f "$(DESKTOP_KEY)" ]; then \
|
||
echo " ключ подписи обновлений: $(DESKTOP_KEY)"; \
|
||
cd $(DESKTOP_TAURI_DIR) && \
|
||
TAURI_SIGNING_PRIVATE_KEY="$$(cat $(DESKTOP_KEY))" \
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-glchat-local-dev}" \
|
||
$(TAURI_CLI) build $(DESKTOP_BUNDLES_ARG); \
|
||
else \
|
||
echo " ключа $(DESKTOP_KEY) нет: сборка без артефактов автообновления"; \
|
||
cd $(DESKTOP_TAURI_DIR) && \
|
||
$(TAURI_CLI) build $(DESKTOP_BUNDLES_ARG) \
|
||
--config '{"bundle":{"createUpdaterArtifacts":false}}'; \
|
||
fi
|
||
|
||
.PHONY: desktop-run
|
||
desktop-run: ## Запустить собранное приложение (macOS); доп. аргументы — ARGS=…
|
||
open -a "$(DESKTOP_TAURI_DIR)/target/release/bundle/macos/glchat.app" $(ARGS)
|
||
|
||
.PHONY: desktop-build-app
|
||
desktop-build-app: ## Собрать .app и артефакты обновления без .dmg (dmg требует Finder/AppleScript)
|
||
@$(MAKE) --no-print-directory desktop-build DESKTOP_BUNDLES=app
|
||
|
||
# Каталог обновлений инстанса и его публичный адрес (docs/client-tauri.md §4).
|
||
# Раскладку делает scripts/desktop-release.sh: манифест версии + latest.json,
|
||
# артефакт и подпись; приложение отдаёт их по /updates/{target}/{arch}/{version}.
|
||
DESKTOP_UPDATES_DIR ?= /opt/glchat/updates
|
||
DESKTOP_BASE_URL ?= https://gl.mhspx.su
|
||
|
||
.PHONY: desktop-release
|
||
desktop-release: ## Разложить артефакты обновления в DESKTOP_UPDATES_DIR (нужен sudo)
|
||
bash scripts/desktop-release.sh --dir "$(DESKTOP_UPDATES_DIR)" \
|
||
--base-url "$(DESKTOP_BASE_URL)" $(ARGS)
|
||
|
||
.PHONY: desktop-release-upload
|
||
desktop-release-upload: ## Загрузить обновления на инстанс: make desktop-release-upload HOST=user@server
|
||
bash scripts/desktop-release.sh --remote "$(HOST)" \
|
||
--base-url "$(DESKTOP_BASE_URL)" $(ARGS)
|
||
|
||
.PHONY: docker-build
|
||
docker-build: ## Собрать образ приложения
|
||
docker build \
|
||
--build-arg VERSION=$(VERSION) \
|
||
--build-arg COMMIT=$(COMMIT) \
|
||
--build-arg BUILD_DATE=$(BUILD_DATE) \
|
||
--build-arg GOPROXY=$(GOPROXY) \
|
||
--build-arg GOSUMDB=$(GOSUMDB) \
|
||
-t $(DOCKER_IMAGE):$(DOCKER_TAG) .
|
||
|
||
.PHONY: install-dry-run
|
||
install-dry-run: ## Проверить установщик без изменений системы
|
||
bash deploy/install.sh --dry-run --domain gl.mhspx.su --email admin@gl.mhspx.su --yes
|
||
|
||
.PHONY: install-test
|
||
install-test: ## Прогон install.sh в чистом контейнере Ubuntu 24.04
|
||
bash scripts/test-install.sh
|
||
|
||
.PHONY: profiles-test
|
||
profiles-test: ## Проверить таблицу профилей производительности (AGENT.md 10.10)
|
||
bash scripts/test-profiles.sh
|
||
|
||
.PHONY: backup-test
|
||
backup-test: ## Проверить бэкап и offsite-выгрузку на синтетическом инстансе
|
||
bash scripts/test-backup.sh
|
||
|
||
.PHONY: profiles-env
|
||
profiles-env: ## Сверить .env установленного инстанса с таблицей профилей
|
||
sudo bash scripts/test-profiles.sh --env "$${GLCHAT_ETC_DIR:-/opt/glchat}/.env"
|
||
|
||
.PHONY: ops-test
|
||
ops-test: profiles-test backup-test ## Проверки эксплуатационных скриптов
|
||
|
||
.PHONY: clean
|
||
clean: ## Удалить артефакты сборки
|
||
rm -rf $(BUILD_DIR)
|