diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..4f51664 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/devcontainers/go:dev-1.25-trixie + +SHELL ["/bin/bash", "-c"] + +RUN apt-get update && apt-get -y install --no-install-recommends \ + bash \ + make \ + git \ + curl \ + ca-certificates \ + findutils \ + coreutils + +RUN curl -sSfL https://taskfile.dev/install.sh | sh -s -- -b /usr/local/bin diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a01fa16 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "test-backend", + "build": { + "dockerfile": "Dockerfile" + }, + "postCreateCommand": "go version && task --version && task install-golangci-lint && task install-formatters" +} diff --git a/.github/scripts/extract-versions.sh b/.github/scripts/extract-versions.sh new file mode 100644 index 0000000..ec9b1a6 --- /dev/null +++ b/.github/scripts/extract-versions.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Скрипт для извлечения версий инструментов из Taskfile.yml +# Использование: .github/scripts/extract-versions.sh + +# Путь к Taskfile.yml +TASKFILE="Taskfile.yml" + +# Проверка наличия файла +if [ ! -f "$TASKFILE" ]; then + echo "Ошибка: Файл $TASKFILE не найден" >&2 + exit 1 +fi + +# Извлечение всех переменных из секции vars +echo "Извлекаем переменные из Taskfile.yml:" + +# Определяем начало и конец секции vars +VARS_START=$(grep -n "^vars:" "$TASKFILE" | cut -d: -f1) +if [ -z "$VARS_START" ]; then + echo "Ошибка: секция vars не найдена в $TASKFILE" >&2 + exit 1 +fi + +VARS_START=$((VARS_START + 1)) + +# Ищем следующую секцию после vars или конец файла +NEXT_SECTION=$(tail -n +$VARS_START "$TASKFILE" | grep -n "^[a-z]" | head -1 | cut -d: -f1) +if [ -n "$NEXT_SECTION" ]; then + VARS_END=$((VARS_START + NEXT_SECTION - 2)) +else + VARS_END=$(wc -l < "$TASKFILE") +fi + +# Извлекаем все строки из секции vars +VARS_SECTION=$(sed -n "${VARS_START},${VARS_END}p" "$TASKFILE") + +# Инициализируем ассоциативный массив для хранения переменных +declare -A VARS + +# Извлекаем имя и значение каждой переменной +while IFS= read -r line; do + # Пропускаем пустые строки и строки с комментариями + if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then + continue + fi + + # Извлекаем имя и значение + if [[ "$line" =~ ^[[:space:]]*([A-Z_0-9]+):\ *\'([^\']*)\' ]]; then + var_name=${BASH_REMATCH[1]} + var_value=${BASH_REMATCH[2]} + VARS["$var_name"]="$var_value" + echo "- $var_name: ${VARS[$var_name]}" + elif [[ "$line" =~ ^[[:space:]]*([A-Z_0-9]+):\ *\"([^\"]*)\" ]]; then + var_name=${BASH_REMATCH[1]} + var_value=${BASH_REMATCH[2]} + VARS["$var_name"]="$var_value" + echo "- $var_name: ${VARS[$var_name]}" + elif [[ "$line" =~ ^[[:space:]]*([A-Z_0-9]+):\ *(.*) ]]; then + var_name=${BASH_REMATCH[1]} + var_value=${BASH_REMATCH[2]} + VARS["$var_name"]="$var_value" + echo "- $var_name: ${VARS[$var_name]}" + fi +done <<< "$VARS_SECTION" + +# Находим список модулей +if [ -n "${VARS[MODULES]}" ]; then + MODULES="${VARS[MODULES]}" + echo "- найдены модули: $MODULES" +else + # Если не найдено в vars, пытаемся найти в другом месте (для обратной совместимости) + MODULES=$(sed -n 's/.*MODULES: \(.*\)/\1/p' "$TASKFILE" | head -1) + echo "- модули (из старого формата): $MODULES" +fi + +# Установка переменных GitHub Actions +if [ -n "$GITHUB_ENV" ]; then + echo "Устанавливаем переменные в GITHUB_ENV:" + # Экспортируем все переменные + for var_name in "${!VARS[@]}"; do + echo "$var_name=${VARS[$var_name]}" >> $GITHUB_ENV + echo " $var_name -> GITHUB_ENV" + done + # Для совместимости добавляем MODULES отдельно, если оно не в vars + if [ -z "${VARS[MODULES]}" ] && [ -n "$MODULES" ]; then + echo "MODULES=$MODULES" >> $GITHUB_ENV + echo " MODULES -> GITHUB_ENV" + fi +fi + +if [ -n "$GITHUB_OUTPUT" ]; then + echo "Устанавливаем переменные в GITHUB_OUTPUT:" + # Экспортируем все переменные + for var_name in "${!VARS[@]}"; do + echo "$var_name=${VARS[$var_name]}" >> $GITHUB_OUTPUT + echo " $var_name -> GITHUB_OUTPUT" + done + # Для совместимости добавляем MODULES отдельно, если оно не в vars + if [ -z "${VARS[MODULES]}" ] && [ -n "$MODULES" ]; then + echo "MODULES=$MODULES" >> $GITHUB_OUTPUT + echo " MODULES -> GITHUB_OUTPUT" + fi +fi \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..46e215f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + extract-vars: + name: Extract Variables from Taskfile + runs-on: ubuntu-latest + outputs: + go-version: ${{ steps.extract.outputs.GO_VERSION }} + golangci-lint-version: ${{ steps.extract.outputs.GOLANGCI_LINT_VERSION }} + modules: ${{ steps.extract.outputs.MODULES }} + + steps: + - name: Checkout code + uses: actions/checkout@v4.2.2 + + - name: Extract variables from Taskfile + id: extract + run: | + chmod +x .github/scripts/extract-versions.sh + .github/scripts/extract-versions.sh + + lint: + needs: extract-vars + uses: ./.github/workflows/lint-reusable.yml + with: + modules: ${{ needs.extract-vars.outputs.modules }} + go-version: ${{ needs.extract-vars.outputs.go-version }} + golangci-lint-version: ${{ needs.extract-vars.outputs.golangci-lint-version }} diff --git a/.github/workflows/lint-reusable.yml b/.github/workflows/lint-reusable.yml new file mode 100644 index 0000000..feb169e --- /dev/null +++ b/.github/workflows/lint-reusable.yml @@ -0,0 +1,40 @@ +name: Lint Reusable + +on: + workflow_call: + inputs: + modules: + required: true + type: string + go-version: + required: true + type: string + golangci-lint-version: + required: true + type: string + +jobs: + golangci-lint: + name: Lint all Go modules + runs-on: ubuntu-latest + + steps: + - name: 📦 Checkout code + uses: actions/checkout@v4.2.2 + + - name: 🛠 Set up Go + uses: actions/setup-go@v5.4.0 + with: + go-version: ${{ inputs.go-version }} + + - name: 🐾 Show go.work (debug) + run: cat go.work || echo "❗ go.work not found" + + - name: 📌 Install Task + uses: arduino/setup-task@v2.0.0 + + - name: ✅ Run golangci-lint via Taskfile + env: + MODULES: ${{ inputs.modules }} + GOLANGCI_LINT_VERSION: ${{ inputs.golangci-lint-version }} + run: task lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ec0e71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# env file +.env +/.idea +/.vscode +/.cursor +.DS_Store + +/bin +node_modules +/shared/api/bundles/ +/coverage/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a11c0cc --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,104 @@ +version: "2" # Версия формата конфигурационного файла (v2 — для golangci-lint 2.x) + +run: # Параметры запуска линтера + timeout: 5m # Максимальное время выполнения анализа + relative-path-mode: gomod # Пути к файлам интерпретируются относительно корня go-модуля + issues-exit-code: 1 # Код выхода = 1, если найдены ошибки (полезно для CI) + tests: true # Анализировать также _test.go файлы + modules-download-mode: readonly # Не разрешать загрузку зависимостей во время анализа (безопасно и быстрее на CI) + +output: # Настройки вывода результатов линтинга + formats: + text: # Формат вывода в терминал + print-linter-name: true # Показывать имя линтера рядом с каждым сообщением + print-issued-lines: true # Показывать строку кода, где найдена проблема + colors: true # Использовать цветной вывод (для лучшей читаемости) + +issues: # Общие настройки для обработки проблем + max-issues-per-linter: 0 # Без лимита на количество ошибок от одного линтера + max-same-issues: 0 # Без лимита на количество одинаковых ошибок + uniq-by-line: true # Отображать максимум одну ошибку на строку (избегает спама от разных линтеров по одной строке) + +linters: # Список активных и отключённых линтеров + default: standard # Использовать стандартный набор линтеров golangci-lint + enable: # Явно включенные дополнительные линтеры + - errcheck # Проверяет, что ошибки не игнорируются + - staticcheck # Глубокий статический анализ (includes gosimple, stylecheck) + - govet # Встроенный в Go инструмент анализа (ловит потенциальные баги) + - gocritic # Набор продвинутых правил для улучшения качества кода + - revive # Проверяет стиль кода, оформление, названия + - unused # Находит неиспользуемые переменные, типы, функции + - gosec # Проверка на уязвимости и небезопасные практики + - depguard # Запрещает импорт указанных пакетов (контроль зависимостей) + - bodyclose # Проверяет, что закрывается `resp.Body` у HTTP-запросов + - asciicheck # Предупреждает о не-ASCII символах в коде + - cyclop # Контролирует цикломатическую сложность функций + - dupl # Находит дублированные фрагменты кода + - ineffassign # Обнаруживает неиспользуемые присваивания + - unparam # Находит неиспользуемые параметры функций + - errorlint # Рекомендует использовать `errors.Is` / `errors.As` вместо прямых сравнений + - errname # Проверяет имена переменных/типов ошибок (должны содержать "Err") + - forbidigo # Запрещает определённые вызовы или конструкции по regex-паттернам + - contextcheck # Проверяет, что `context.Context` передаётся в методы/функции + - containedctx # Предупреждает, если `context.Context` сохраняется в структуре (плохо) + disable: # Линтеры, которые намеренно отключены + - gocyclo # Старый линтер сложности (заменён на cyclop) + - lll # Проверка длины строк (часто шумная и мешает) + + exclusions: # Исключения из анализа + generated: strict # Игнорировать сгенерированные файлы (по "// Code generated ... DO NOT EDIT.") + rules: # Локальные исключения по пути/файлу/линерам + - path: _test\.go # Для всех тестов: + linters: + - cyclop # Не проверять сложность тестов + - dupl # Не проверять дублирование в тестах + - gosec # Не искать уязвимости в тестах + + settings: # Индивидуальные настройки для конкретных линтеров + + gosec: + # Включаем все правила, включая экспериментальные + config: + global: + audit: true # Активировать все правила безопасности + show-ignored: true # Показывать проигнорированные проблемы в комментариях + severity: "medium" # Минимальная важность проблем, high/medium/low + confidence: "medium" # Минимальный уровень уверенности, high/medium/low + + cyclop: + max-complexity: 20 # Допустимая сложность функции (больше 10, но не слишком жёстко) + + depguard: + rules: + main: + deny: + - pkg: io/ioutil # Запрет использовать устаревший пакет + desc: "Использование устаревшего пакета io/ioutil запрещено (замените вызовы на аналоги из пакетов os/io)" + + revive: + severity: warning # Нарушения revive будут как предупреждения, а не ошибки + + forbidigo: + exclude-godoc-examples: true # Не применять запреты к Godoc-примерам + analyze-types: true # Искать запреты и в типах/константах, а не только в функциях + forbid: # Список запрещённых паттернов + - pattern: '^fmt\.Print.*$' # Запрет использования fmt.Print*, fmt.Println и т.д. + msg: "Запрещено напрямую использовать fmt.Print* для логирования (вместо этого используйте структурированный логгер)" + - pattern: '^time\.Sleep$' # Запрет использования time.Sleep + msg: "Запрещено использовать time.Sleep в продакшен-коде (используй таймеры/контекст)" + - pattern: '^http\.DefaultClient$' # Запрет использования небезопасного http.DefaultClient + msg: "Не используй http.DefaultClient (нет таймаутов); создай *http.Client с таймаутами" + +formatters: # Форматтеры кода (проверка стиля) + enable: + - gofumpt # Строгая версия gofmt с дополнительными правилами + - gci # Форматтер импортов (гибкая альтернатива goimports) + settings: + gofumpt: + extra-rules: true # Включить дополнительные строгие правила (например, удаление лишних пустых строк) + gci: + sections: # Порядок группировки импортов + - Standard # Стандартная библиотека + - Default # Сторонние зависимости + - Prefix(github.com/qyrlabs/test-backend) # Локальные импорты проекта + no-inline-comments: false # Разрешить комментарии после импортов (true — запретит) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cbcdbd --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# test-backend diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..aa0bc20 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,112 @@ +version: '3' + +# Глобальные переменные проекта +vars: + GO_VERSION: '1.25.6' + GOLANGCI_LINT_VERSION: 'v2.8.0' + GCI_VERSION: 'v0.13.7' + GOFUMPT_VERSION: 'v0.9.2' + + BIN_DIR: '{{.ROOT_DIR}}/bin' + GOLANGCI_LINT: '{{.BIN_DIR}}/golangci-lint' + GCI: '{{.BIN_DIR}}/gci' + GOFUMPT: '{{.BIN_DIR}}/gofumpt' + MODULES: inventory order payment shared + +tasks: + install-formatters: + desc: "Устанавливает форматтеры gci и gofumpt в ./bin" + summary: | + Эта задача проверяет наличие инструментов форматирования кода gofumpt и gci в директории bin. + Если инструменты не найдены, они будут автоматически установлены с указанными версиями. + + Используется: + - gofumpt: для форматирования кода Go + - gci: для сортировки импортов Go + cmds: + - | + [ -f {{.GOFUMPT}} ] || { + echo '📦 Устанавливаем gofumpt {{.GOFUMPT_VERSION}}...' + GOBIN={{.BIN_DIR}} go install mvdan.cc/gofumpt@{{.GOFUMPT_VERSION}} + } + [ -f {{.GCI}} ] || { + echo '📦 Устанавливаем gci {{.GCI_VERSION}}...' + GOBIN={{.BIN_DIR}} go install github.com/daixiang0/gci@{{.GCI_VERSION}} + } + status: + - test -x {{.GOFUMPT}} + - test -x {{.GCI}} + + format: + desc: "Форматирует весь проект gofumpt + gci, исключая mocks" + summary: | + Форматирует все Go-файлы проекта с использованием gofumpt для стандартизации кода + и gci для сортировки импортов, исключая файлы в директориях mocks. + + Использует инструменты: + - gofumpt: для стандартизации форматирования + - gci: для сортировки импортов по стандартным группам + deps: [ install-formatters ] + cmds: + - | + echo "🧼 Форматируем через gofumpt ..." + + for module in {{.MODULES}}; do + if [ -d "$module" ]; then + echo "🧼 Форматируем $module" + find $module -type f -name '*.go' ! -path '*/mocks/*' -exec {{.GOFUMPT}} -extra -w {} + + fi + done + - | + echo "🎯 Сортируем импорты через gci ..." + + for module in {{.MODULES}}; do + if [ -d "$module" ]; then + echo "🎯 Сортируем импорты в $module" + find $module -type f -name '*.go' ! -path '*/mocks/*' -exec {{.GCI}} write -s standard -s default -s "prefix(github.com/qyrlabs/test-backend)" {} + + fi + done + + install-golangci-lint: + desc: "Устанавливает golangci-lint в каталог bin" + summary: | + Проверяет наличие golangci-lint в директории bin. + Если инструмент не найден, автоматически устанавливает его через go install. + + Устанавливаемая версия: {{.GOLANGCI_LINT_VERSION}} + cmds: + - | + [ -f {{.GOLANGCI_LINT}} ] || { + mkdir -p {{.BIN_DIR}} + echo "📦 Устанавливаем golangci-lint {{.GOLANGCI_LINT_VERSION}}..." + GOBIN={{.BIN_DIR}} go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@{{.GOLANGCI_LINT_VERSION}} + } + status: + - test -x {{.GOLANGCI_LINT}} + + lint: + desc: "Запускает golangci-lint для всех модулей" + summary: | + Запускает линтер golangci-lint для всех модулей проекта. + Линтер проверяет код на соответствие стандартам качества и лучшим практикам. + Проверка включает проверку безопасности через gosec (встроенный в golangci-lint). + + Зависимости: + - install-golangci-lint: автоматически устанавливает линтер + - format: форматирует код перед проверкой + deps: [ install-golangci-lint ] + vars: + MODULES: '{{.MODULES}}' + GOLANGCI_LINT: '{{.GOLANGCI_LINT}}' + cmds: + - | + set -e + ERR=0 + echo "🔍 Линтим все модули ..." + for mod in {{.MODULES}}; do + if [ -d "$mod" ]; then + echo "🔍 Линтим $mod module" + {{.GOLANGCI_LINT}} run $mod/... --config=.golangci.yml || ERR=1 + fi + done + exit $ERR diff --git a/go.work b/go.work new file mode 100644 index 0000000..a44dbea --- /dev/null +++ b/go.work @@ -0,0 +1,8 @@ +go 1.25.6 + +use ( + ./inventory + ./order + ./payment + ./shared +) diff --git a/inventory/go.mod b/inventory/go.mod new file mode 100644 index 0000000..89c7d5d --- /dev/null +++ b/inventory/go.mod @@ -0,0 +1,3 @@ +module github.com/qyrlabs/test-backend/inventory + +go 1.25.6 diff --git a/inventory/main.go b/inventory/main.go new file mode 100644 index 0000000..3b2e8d2 --- /dev/null +++ b/inventory/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "log" +) + +func main() { + log.Println("inventory up") +} diff --git a/order/go.mod b/order/go.mod new file mode 100644 index 0000000..3d8ec22 --- /dev/null +++ b/order/go.mod @@ -0,0 +1,3 @@ +module github.com/qyrlabs/test-backend/order + +go 1.25.6 diff --git a/order/main.go b/order/main.go new file mode 100644 index 0000000..567b486 --- /dev/null +++ b/order/main.go @@ -0,0 +1,7 @@ +package main + +import "log" + +func main() { + log.Println("order up") +} diff --git a/payment/go.mod b/payment/go.mod new file mode 100644 index 0000000..b352d61 --- /dev/null +++ b/payment/go.mod @@ -0,0 +1,3 @@ +module github.com/qyrlabs/test-backend/payment + +go 1.25.6 diff --git a/payment/main.go b/payment/main.go new file mode 100644 index 0000000..2fe1a5f --- /dev/null +++ b/payment/main.go @@ -0,0 +1,7 @@ +package main + +import "log" + +func main() { + log.Println("payment up") +} diff --git a/shared/go.mod b/shared/go.mod new file mode 100644 index 0000000..43dcdf3 --- /dev/null +++ b/shared/go.mod @@ -0,0 +1,3 @@ +module github.com/qyrlabs/test-backend/shared + +go 1.25.6 diff --git a/shared/main.go b/shared/main.go new file mode 100644 index 0000000..55470b6 --- /dev/null +++ b/shared/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "log" +) + +func main() { + log.Println("shared up") +}