commit 49b389ed533ff664eaa447f597f381bf9f5e75e2 Author: V.Bolshakov Date: Mon May 4 16:54:53 2026 +0700 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0be3778 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Переопределения дефолтных значений из docker-compose.yml. +# Файл опциональный — без него стек запустится с дефолтами. +# Скопировать в .env и заполнить нужные значения. + +# ============================================================================= +# n8n +# ============================================================================= + +# URL для вебхуков (адрес, по которому n8n доступен снаружи) +WEBHOOK_URL=http://YOUR_SERVER_IP:5678 + +# Пароль PostgreSQL +DB_POSTGRESDB_PASSWORD= + +# Токен аутентификации между n8n и task runner +N8N_RUNNERS_AUTH_TOKEN= + +# Ключ шифрования credentials в БД (зафиксировать после первого старта!) +N8N_ENCRYPTION_KEY= + +# Часовой пояс +GENERIC_TIMEZONE=Europe/Moscow + +# ============================================================================= +# Grafana +# ============================================================================= + +GF_SECURITY_ADMIN_USER=admin +GF_SECURITY_ADMIN_PASSWORD= + +# ============================================================================= +# PostgreSQL +# ============================================================================= + +DB_POSTGRESDB_DATABASE=n8n +DB_POSTGRESDB_USER=postgres diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d8b957 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Секреты — не коммитить +.env + +# Логи +n8n/logs/*.log + +# Бинарные данные +n8n/shared/binaryData/ + +# Бэкапы с реальными данными (оставить только .gitkeep и служебные workflow) +n8n/backup/workflows/*.json +n8n/backup/credentials/*.json +!n8n/backup/workflows/.gitkeep +!n8n/backup/credentials/.gitkeep +!n8n/backup/credentials/n8n_local.json +!n8n/backup/credentials/header_n8n_local.json +!n8n/backup/workflows/Backup_Workflows.json +!n8n/backup/workflows/Git_Commit_Workflows.json +!n8n/backup/workflows/Git_Pull_Workflows.json +!n8n/backup/workflows/Backup_Сredentials.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5d9148 --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# n8n Docker Stack + +Self-hosted n8n с PostgreSQL, Redis, Task Runners, Prometheus и Grafana — готовый production-стек с минимальными усилиями на настройку + +## Зачем + +- **Изоляция** — отключена телеметрия, шаблоны и внешние запросы к n8n.io +- **Производительность** — queue mode с workers, внешний task runner для JS и Python +- **Надёжность** — PostgreSQL вместо SQLite, Redis как брокер очередей +- **Наблюдаемость** — Prometheus + Grafana с готовым дашбордом +- **Воспроизводимость** — автоимпорт workflows и credentials при каждом старте +- **Контроль версий** — встроенные workflows для экспорта и коммита в git + +--- + +## Сервисы + +| Сервис | Порт | Описание | +|---|---|---| +| n8n | 5678 | Основной сервер | +| n8n-worker-1 | — | Worker для queue mode | +| n8n-runner-1 | — | Внешний раннер JS + Python | +| postgres | 5433 | База данных | +| redis (Valkey) | — | Очередь Bull | +| prometheus | 9090 | Сбор метрик | +| grafana | 3000 | Дашборды | +| fix-permissions | — | Однократная правка прав (uid 1000) | +| n8n-import | — | Автоимпорт workflows и credentials при старте | + +--- + +## Быстрый старт + +```bash +cp .env.example .env +# Заполнить .env + +docker compose up -d +``` + +Стек запускается без `.env` — все переменные имеют дефолты в `docker-compose.yml`. + +--- + +## Деплой через Portainer + +Portainer позволяет управлять стеком через UI без прямого доступа к серверу. + +**Важно:** деплой через «URL» (одиночная ссылка на compose-файл) не работает — Portainer не скачивает вспомогательные файлы (`prometheus.yml`, `grafana/` и т.д.). Использовать только **Repository**. + +### Требования + +Portainer должен быть запущен так, чтобы **путь к данным на хосте и внутри контейнера совпадал**. Иначе Docker Engine не найдёт файлы клонированного репозитория — Portainer передаёт ему свои внутренние пути, а Docker ищет их на хосте. + +```bash +docker stop portainer && docker rm portainer + +PORTAINER_DATA="$HOME/portainer/data" +mkdir -p "$PORTAINER_DATA" + +docker run -d \ + -p 9443:9443 \ + --name portainer \ + --restart=always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PORTAINER_DATA":"$PORTAINER_DATA" \ + portainer/portainer-ce:latest \ + --data "$PORTAINER_DATA" +``` + +Флаг `--data` указывает Portainer хранить данные по тому же пути, что смонтирован с хоста — пути совпадают, Docker Engine находит файлы. + +### Шаги + +1. **Stacks → Add stack → Repository** +2. Repository URL: адрес репозитория +3. Compose path: `docker-compose.yml` +4. Добавить переменные окружения или указать путь к `.env` +Важно правильно настроить WEBHOOK_URL +``` +# URL для вебхуков (адрес, по которому n8n доступен снаружи) +WEBHOOK_URL=http://YOUR_SERVER_IP:5678 +``` +⚠️ Важно: Если указан неверный адрес, то Form Data и вебхуки не будут работать + +5. Deploy the stack + +> (Опционально) Дать права +```sh +sudo chmod 777 /home/bbr/portainer/data/compose +``` + +### Документация + +- [Установка Docker Compose](https://docs.n8n.io/hosting/installation/server-setups/docker-compose/) +- [Stacks in Portainer](https://docs.portainer.io/user/docker/stacks/add) +- [Git repository deployment](https://docs.portainer.io/user/docker/stacks/add#option-3-git-repository) + +--- + +## Встроенные workflows + +В `n8n/backup/workflows/` лежат служебные workflow, которые импортируются автоматически при каждом старте. + +| Файл | Назначение | +|---|---| +| `Backup_Workflows.json` | Экспорт всех workflow в `n8n/shared/` | +| `Backup_Сredentials.json` | Экспорт всех credentials в `n8n/shared/` | +| `Git_Commit_Workflows.json` | Коммит экспортированных файлов в git | +| `Git_Pull_Workflows.json` | Pull изменений из git-репозитория | + +### Настройка credentials + +Встроенные workflows обращаются к API локального n8n. Для работы необходимо: + +1. Сгенерировать API ключ: n8n → **Settings → API → Create an API key** +2. Создать credentials типа **n8n API**: + - **URL**: `http://n8n:5678/api/v1` + - **API Key**: ключ из шага 1 +3. Сохранить credentials в файл `n8n/backup/credentials/n8n_local.json` + +### Защита credentials от перезаписи + +n8n CLI не имеет встроенного `--skipExisting` для импорта credentials. Скрипт реализует это через маркер-файлы в `n8n_storage` volume (`/home/node/.n8n/imported/`). + +| Тип | Поведение при повторном деплое | +|---|---| +| Credentials | Импортируются **один раз**, затем пропускаются | +| Workflows | Перезаписываются **всегда** | + +Принудительный переимпорт — задать `FORCE_IMPORT=true` в env сервиса `n8n-import`. + +Для добавления своих workflow при старте — положить JSON в `n8n/backup/workflows/` или `n8n/backup/credentials/`. + +### Документация + +- [n8n Public API / Authentication](https://docs.n8n.io/api/authentication/) +- [n8n CLI commands](https://docs.n8n.io/hosting/cli-commands/) +- [Автоимпорт workflows](https://docs.n8n.io/hosting/cli-commands/#import-workflows-and-credentials) + +--- + +## Политика хранения данных + +| Что | Лимит | +|---|---| +| Данные выполнений | 7 дней / 5 000 записей | +| История версий workflow | 30 дней | +| Логи n8n (файлы) | 10 файлов × 20 MB = ~200 MB | +| Логи Docker (все сервисы) | 3–5 файлов × 5–10 MB на сервис | +| Prometheus TSDB | 15 дней / 1 GB | +| Redis | 256 MB max (LRU) | + +Бинарные данные в `n8n/shared/` автоматически не очищаются — при активном использовании нод Read/Write Files настроить очистку по расписанию. + +### Документация + +- [Execution data — pruning и retention](https://docs.n8n.io/hosting/scaling/execution-data/) +- [Executions environment variables](https://docs.n8n.io/hosting/configuration/environment-variables/executions/) +- [Workflow history](https://docs.n8n.io/workflows/history/) +- [Логирование](https://docs.n8n.io/hosting/logging-monitoring/logging/) +- [Environment Variables (все)](https://docs.n8n.io/hosting/configuration/environment-variables/) +- [Task Runners](https://docs.n8n.io/hosting/configuration/task-runners/) +- [Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) +- [Мониторинг n8n](https://docs.n8n.io/hosting/logging-monitoring/monitoring/) +- [Prometheus](https://docs.n8n.io/hosting/configuration/configuration-examples/prometheus/) +- [Аудит безопасности](https://docs.n8n.io/hosting/securing/security-audit/) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3b5716f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,439 @@ +# тест тест + +volumes: + n8n_storage: + postgres_data: + valkey-data: + grafana-data: + prometheus_data: + +x-n8n: &service-n8n + image: n8nio/n8n:stable + restart: unless-stopped + environment: + WEBHOOK_URL: ${WEBHOOK_URL:-} + N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:-} + # Изоляция self-hosted + # https://docs.n8n.io/hosting/configuration/configuration-examples/isolation/ + # ============================================================================= + # N8N_DIAGNOSTICS_ENABLED - Отправлять анонимную телеметрию (false отключает Ask AI в Code node). По умолчанию: true. + # N8N_VERSION_NOTIFICATIONS_ENABLED - Уведомления о новых версиях. По умолчанию: true. + # N8N_TEMPLATES_ENABLED - Включить шаблоны workflow. По умолчанию: false. + # N8N_HIRING_BANNER_ENABLED - Баннер о вакансиях в консоли. По умолчанию: true. + N8N_DIAGNOSTICS_ENABLED: false + N8N_VERSION_NOTIFICATIONS_ENABLED: false + N8N_TEMPLATES_ENABLED: false + N8N_HIRING_BANNER_ENABLED: false + + # Настройки логирования + # https://docs.n8n.io/hosting/configuration/environment-variables/logs/ + # ============================================================================= + # N8N_LOG_LEVEL - Уровень логирования: info|warn|error|debug. По умолчанию: info. + # N8N_LOG_OUTPUT - Куда выводить: console|file (через запятую). По умолчанию: console. + # N8N_LOG_FORMAT - Формат: text|json. По умолчанию: text. + # N8N_LOG_FILE_LOCATION - Путь к файлу лога. По умолчанию: ~/.n8n/logs/n8n.log. + # N8N_LOG_FILE_SIZE_MAX - Максимальный размер файла лога (MB). По умолчанию: 16. + # DB_LOGGING_ENABLED - Логирование запросов к БД. По умолчанию: false. + # DB_LOGGING_OPTIONS - Уровень логирования БД: query|error|schema|warn|info|log|all. По умолчанию: error. + # CODE_ENABLE_STDOUT - Выводить console.log из Code node в stdout. По умолчанию: false. + + N8N_LOG_LEVEL: info + N8N_LOG_OUTPUT: console,file + N8N_LOG_FORMAT: json + N8N_LOG_FILE_LOCATION: /data/logs/n8n.log + N8N_LOG_FILE_SIZE_MAX: 20 + N8N_LOG_FILE_COUNT_MAX: 10 + DB_LOGGING_ENABLED: true + DB_LOGGING_OPTIONS: error + CODE_ENABLE_STDOUT: false + + # Персонализация + # ============================================================================= + # N8N_PERSONALIZATION_ENABLED - Вопросы персонализации при первом запуске. По умолчанию: true. + # N8N_HIDE_USAGE_PAGE - Скрыть страницу использования и планов. По умолчанию: false. + N8N_PERSONALIZATION_ENABLED: false + N8N_HIDE_USAGE_PAGE: true + + # Настройки узлов (Nodes) + # https://docs.n8n.io/hosting/configuration/environment-variables/nodes/ + # NODES_EXCLUDE: Список узлов, которые не нужно загружать (блокировка). Пример: ["n8n-nodes-baseexecuteCommand"]. + NODES_EXCLUDE: ${NODES_EXCLUDE:-[]} + + # Настройки безопасности + # https://docs.n8n.io/hosting/configuration/environment-variables/security/ + # N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES - Блокировать доступ к файлам .n8n и конфигам. По умолчанию: true. + # N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS - Права 0600 на файл настроек. По умолчанию: false. + # N8N_RESTRICT_FILE_ACCESS_TO - Директории с разрешенным доступом к файлам (через ;). По умолчанию: "". + + N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: true + N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: false + N8N_RESTRICT_FILE_ACCESS_TO: /data/shared;/data/logs + + # Настройки часового пояса и локализации + # https://docs.n8n.io/hosting/configuration/environment-variables/timezone-localization/ + # ============================================================================= + # GENERIC_TIMEZONE - Часовой пояс (важно для Schedule/Cron узлов). По умолчанию: America/New_York. + # N8N_DEFAULT_LOCALE - Локаль интерфейса (региональные идентификаторы не поддерживаются). По умолчанию: en. + GENERIC_TIMEZONE: ${GENERIC_TIMEZONE:-Europe/Moscow} + N8N_DEFAULT_LOCALE: ${N8N_DEFAULT_LOCALE:-en} + + # Настройки базы данных PostgreSQL + # https://docs.n8n.io/hosting/configuration/environment-variables/database/ + # ============================================================================= + # -- Параметры подключения + # DB_POSTGRESDB_DATABASE - Имя базы данных PostgreSQL(по умолчанию: n8n) + # DB_POSTGRESDB_HOST - Хост PostgreSQL сервера(по умолчанию: localhost) + # DB_POSTGRESDB_PORT - Порт PostgreSQL сервера(по умолчанию: 5432) + # DB_POSTGRESDB_USER - Имя пользователя PostgreSQL(по умолчанию: postgres) + # DB_POSTGRESDB_PASSWORD - Пароль PostgreSQL(обязательное поле, нет значения по умолчанию) + + DB_TYPE: postgresdb + DB_POSTGRESDB_DATABASE: ${DB_POSTGRESDB_DATABASE:-n8n} + DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST:-postgres} + DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT:-5432} + DB_POSTGRESDB_USER: ${DB_POSTGRESDB_USER:-postgres} + DB_POSTGRESDB_PASSWORD: ${DB_POSTGRESDB_PASSWORD:-your_secure_password_here} + + # Настройки Task Runner + # https://docs.n8n.io/hosting/configuration/environment-variables/task-runners/ + # ============================================================================= + # N8N_RUNNERS_ENABLED - Включить task runners (рекомендуется для production). По умолчанию: false. + # N8N_RUNNERS_MODE - Режим: internal|external. По умолчанию: internal. + # N8N_RUNNERS_BROKER_LISTEN_ADDRESS - Адрес брокера (127.0.0.1 для internal, 0.0.0.0 для external). По умолчанию: 127.0.0.1. + # N8N_RUNNERS_AUTH_TOKEN - Токен аутентификации (обязателен для external, авто для internal). По умолчанию: "". + # N8N_RUNNERS_MAX_CONCURRENCY - Одновременных задач на runner'е. По умолчанию: 5. + # N8N_RUNNERS_TASK_TIMEOUT - Макс. время выполнения задачи (сек), после — runner перезапускается. По умолчанию: 300. + # N8N_RUNNERS_INSECURE_MODE - Отключить все меры безопасности (НЕ ДЛЯ PRODUCTION!). По умолчанию: false. + # N8N_RUNNERS_TASK_REQUEST_TIMEOUT - Таймаут ожидания свободного runner'а (сек). По умолчанию: 20. + + N8N_RUNNERS_ENABLED: true + N8N_RUNNERS_MODE: external + N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:-your-secret-here} + N8N_RUNNERS_BROKER_PORT: 5679 + N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0 + N8N_RUNNERS_MAX_CONCURRENCY: ${N8N_RUNNERS_MAX_CONCURRENCY:-5} + N8N_RUNNERS_TASK_TIMEOUT: ${N8N_RUNNERS_TASK_TIMEOUT:-300} + N8N_RUNNERS_INSECURE_MODE: ${N8N_RUNNERS_INSECURE_MODE:-false} + N8N_RUNNERS_TASK_REQUEST_TIMEOUT: ${N8N_RUNNERS_TASK_REQUEST_TIMEOUT:-20} + + # Настройки режима очереди (Queue mode) + # https://docs.n8n.io/hosting/configuration/environment-variables/queue-mode/ + # ============================================================================= + # EXECUTIONS_MODE - Режим выполнения: regular|queue. По умолчанию: regular. + # OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS - Ручные запуски на worker'ах. По умолчанию: false. + # QUEUE_BULL_REDIS_HOST - Хост Redis. По умолчанию: localhost. + # QUEUE_BULL_REDIS_PORT - Порт Redis. По умолчанию: 6379. + # QUEUE_HEALTH_CHECK_ACTIVE - Health checks. По умолчанию: false. + + EXECUTIONS_MODE: queue + OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: true + QUEUE_BULL_REDIS_HOST: ${REDIS_HOST:-redis} + QUEUE_BULL_REDIS_PORT: ${REDIS_PORT:-6379} + QUEUE_HEALTH_CHECK_ACTIVE: true + + # Настройки мониторинга + # https://docs.n8n.io/hosting/configuration/environment-variables/endpoints/ + # ============================================================================= + + # N8N_METRICS - Включить endpoint /metrics. По умолчанию: false. + # N8N_METRICS_PREFIX - Префикс для метрик. По умолчанию: n8n_. + # N8N_METRICS_INCLUDE_DEFAULT_METRICS - Стандартные метрики системы и node.js. По умолчанию: true. + # N8N_METRICS_INCLUDE_CACHE_METRICS - Метрики кэша. По умолчанию: false. + # N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS - Метрики шины событий. По умолчанию: false. + # N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL - Label с ID workflow. По умолчанию: false. + # N8N_METRICS_INCLUDE_NODE_TYPE_LABEL - Label с типом узла. По умолчанию: false. + # N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL - Label с типом credentials. По умолчанию: false. + # N8N_METRICS_INCLUDE_API_ENDPOINTS - Метрики для API endpoints. По умолчанию: false. + # N8N_METRICS_INCLUDE_API_PATH_LABEL - Label с путем API. По умолчанию: false. + # N8N_METRICS_INCLUDE_API_METHOD_LABEL - Label с HTTP методом. По умолчанию: false. + # N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL - Label с HTTP статус-кодом. По умолчанию: false. + # N8N_METRICS_INCLUDE_QUEUE_METRICS - Метрики очереди (для scaling mode). По умолчанию: false. + # N8N_METRICS_QUEUE_METRICS_INTERVAL - Частота обновления метрик очереди (сек). По умолчанию: 20. + + N8N_METRICS: ${N8N_METRICS:-true} + N8N_METRICS_PREFIX: ${N8N_METRICS_PREFIX:-n8n_} + N8N_METRICS_INCLUDE_DEFAULT_METRICS: ${N8N_METRICS_INCLUDE_DEFAULT_METRICS:-true} + N8N_METRICS_INCLUDE_CACHE_METRICS: ${N8N_METRICS_INCLUDE_CACHE_METRICS:-false} + N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS: ${N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS:-false} + N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL: ${N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL:-true} + N8N_METRICS_INCLUDE_NODE_TYPE_LABEL: ${N8N_METRICS_INCLUDE_NODE_TYPE_LABEL:-true} + N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL: ${N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL:-false} + N8N_METRICS_INCLUDE_API_ENDPOINTS: ${N8N_METRICS_INCLUDE_API_ENDPOINTS:-false} + N8N_METRICS_INCLUDE_API_PATH_LABEL: ${N8N_METRICS_INCLUDE_API_PATH_LABEL:-false} + N8N_METRICS_INCLUDE_API_METHOD_LABEL: ${N8N_METRICS_INCLUDE_API_METHOD_LABEL:-false} + N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL: ${N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL:-false} + N8N_METRICS_INCLUDE_QUEUE_METRICS: ${N8N_METRICS_INCLUDE_QUEUE_METRICS:-true} + N8N_METRICS_QUEUE_METRICS_INTERVAL: ${N8N_METRICS_QUEUE_METRICS_INTERVAL:-20} + + N8N_USER_MANAGEMENT_DISABLED: false + N8N_SECURE_COOKIE: false + + # Политика хранения данных выполнений + # https://docs.n8n.io/hosting/configuration/environment-variables/executions/ + # ============================================================================= + # EXECUTIONS_DATA_MAX_AGE - Максимальный возраст данных (часы). По умолчанию: 336. + # EXECUTIONS_DATA_PRUNE_MAX_COUNT - Максимум хранимых выполнений. По умолчанию: 10000. + # N8N_WORKFLOW_HISTORY_PRUNE_TIME - Хранить версии workflow (часы, -1 = бессрочно). По умолчанию: -1. + # N8N_INSIGHTS_COMPACTION_HOURLY_TO_DAILY_THRESHOLD_DAYS - Компактизация часовых → дневных (дни). По умолчанию: 90. + # N8N_INSIGHTS_COMPACTION_DAILY_TO_WEEKLY_THRESHOLD_DAYS - Компактизация дневных → недельных (дни). По умолчанию: 180. + + EXECUTIONS_DATA_PRUNE: true + EXECUTIONS_DATA_MAX_AGE: 168 # 7 дней + EXECUTIONS_DATA_PRUNE_MAX_COUNT: 5000 + EXECUTIONS_DATA_HARD_DELETE_BUFFER: 1 + EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL: 15 + EXECUTIONS_DATA_PRUNE_SOFT_DELETE_INTERVAL: 60 + + N8N_WORKFLOW_HISTORY_PRUNE_TIME: 720 # 30 дней + + N8N_INSIGHTS_COMPACTION_HOURLY_TO_DAILY_THRESHOLD_DAYS: 30 + N8N_INSIGHTS_COMPACTION_DAILY_TO_WEEKLY_THRESHOLD_DAYS: 60 + + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + extra_hosts: + - "kontur.bbr.ru:192.168.1.155" + - "bankrupt.bbr.ru:192.168.1.155" + - "forge.bbr.ru:192.168.1.155" + - "forge2.bbr.ru:192.168.1.156" + +x-n8n-runner: &service-n8n-runner + build: + context: ./n8n/runner + dockerfile: Dockerfile.runner + pull: true + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +services: + fix-permissions: + image: alpine:3 + container_name: fix-permissions + restart: "no" + user: root + # n8n, n8n-worker-1, n8n-runner-1 запускаются от uid 1000 (node) + # После клонирования через Portainer директории принадлежат root — исправляем + command: chown -R 1000:1000 /data/shared /data/logs /backup + volumes: + - ./n8n/shared:/data/shared + - ./n8n/logs:/data/logs + - ./n8n/backup:/backup + logging: + driver: json-file + options: + max-size: "1m" + max-file: "1" + + n8n: + <<: *service-n8n + container_name: n8n + restart: unless-stopped + volumes: + - n8n_storage:/home/node/.n8n + - ./n8n/shared:/data/shared + - ./n8n/logs:/data/logs + ports: + - "5678:5678" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + depends_on: + fix-permissions: + condition: service_completed_successfully + postgres: + condition: service_healthy + + n8n-worker-1: + <<: *service-n8n + command: worker + container_name: n8n-worker-1 + volumes: + - n8n_storage:/home/node/.n8n + - ./n8n/shared:/data/shared + - ./n8n/logs:/data/logs + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + depends_on: + fix-permissions: + condition: service_completed_successfully + n8n: + condition: service_healthy + redis: + condition: service_healthy + postgres: + condition: service_healthy + + n8n-runner-1: + <<: *service-n8n-runner + container_name: n8n-runner-1 + restart: unless-stopped + volumes: + - ./n8n/shared:/data/shared + - ./n8n/logs:/data/logs + entrypoint: + ["/bin/sh", "-c", "/usr/local/bin/task-runner-launcher javascript python"] + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5680/healthz || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + environment: + # N8N_RUNNERS_TASK_BROKER_URI - URI брокера задач. По умолчанию: http://127.0.0.1:5679. + # GENERIC_TIMEZONE - Часовой пояс (важно для Schedule/Cron узлов). По умолчанию: America/New_York. + # N8N_RUNNERS_AUTH_TOKEN - Токен аутентификации (обязателен для external, авто для internal). По умолчанию: "". + # N8N_RUNNERS_LAUNCHER_LOG_LEVEL - Уровень логирования: debug|info|warn|error. По умолчанию: info. + # N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT - Секунд бездействия до остановки runner'а. По умолчанию: 15. + + N8N_RUNNERS_TASK_BROKER_URI: http://n8n-worker-1:5679 + GENERIC_TIMEZONE: ${GENERIC_TIMEZONE:-Europe/Moscow} + N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:-your-secret-here} + N8N_RUNNERS_LAUNCHER_LOG_LEVEL: ${N8N_RUNNERS_LAUNCHER_LOG_LEVEL:-info} + N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: ${N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT:-15} + depends_on: + fix-permissions: + condition: service_completed_successfully + n8n-worker-1: + condition: service_healthy + + postgres: + container_name: postgres + image: postgres:${POSTGRES_VERSION:-17} + restart: unless-stopped + # autovacuum_vacuum_scale_factor=0.05 — запускать VACUUM когда мёртвых строк > 5% таблицы (по умолчанию 20%) + command: > + postgres + -c autovacuum=on + -c autovacuum_vacuum_scale_factor=0.05 + -c autovacuum_analyze_scale_factor=0.02 + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 3s + retries: 10 + environment: + POSTGRES_USER: ${DB_POSTGRESDB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD:-your_secure_password_here} + POSTGRES_DB: ${DB_POSTGRESDB_DATABASE:-n8n} + volumes: + - postgres_data:/var/lib/postgresql/data + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + redis: + container_name: redis + image: docker.io/valkey/valkey:8-alpine + # --save 30 1 — снапшот каждые 30 мин при ≥1 изменении + # --maxmemory 256mb — жёсткий лимит памяти + # --maxmemory-policy allkeys-lru — вытеснять редко используемые ключи при достижении лимита + command: > + valkey-server + --save 30 1 + --loglevel warning + --maxmemory 256mb + --maxmemory-policy allkeys-lru + restart: unless-stopped + stop_grace_period: 30s + volumes: + - valkey-data:/data + cap_drop: + - ALL + cap_add: + - SETGID + - SETUID + - DAC_OVERRIDE + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 3s + timeout: 10s + retries: 10 + logging: + driver: json-file + options: + max-size: "5m" + max-file: "3" + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + # --storage.tsdb.retention.time — удалять данные старше 15 дней + # --storage.tsdb.retention.size — удалять при превышении 1 ГБ (что наступит первым) + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=15d' + - '--storage.tsdb.retention.size=1GB' + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - n8n + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + grafana: + image: grafana/grafana:11.2.0 + container_name: grafana + restart: unless-stopped + environment: + GF_SECURITY_ADMIN_USER: ${GF_SECURITY_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-admin} + GF_LOG_LEVEL: warn + GF_LOG_MODE: console + ports: + - "3000:3000" + volumes: + - grafana-data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/var/lib/grafana/dashboards + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - prometheus + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + n8n-import: + <<: *service-n8n + container_name: n8n-import + restart: "no" + entrypoint: /bin/sh + command: /scripts/import_workflows.sh + volumes: + - n8n_storage:/home/node/.n8n + - ./n8n/shared:/data/shared + - ./n8n/logs:/data/logs + - ./n8n/backup:/backup + - ./scripts/import_workflows.sh:/scripts/import_workflows.sh:ro + depends_on: + n8n: + condition: service_healthy diff --git a/grafana/dashboards/n8n_monitoring.json b/grafana/dashboards/n8n_monitoring.json new file mode 100644 index 0000000..1d76f1a --- /dev/null +++ b/grafana/dashboards/n8n_monitoring.json @@ -0,0 +1,1798 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Dashboard for monitoring n8n instances with instance selector", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Version" + } + ] + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_version_info", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "expr": "n8n_nodejs_version_info", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Version Information", + "transformations": [ + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value #A": true, + "Value #B": true, + "__name__": true, + "instance": true, + "job": false, + "major": true, + "minor": true, + "patch": true, + "version": false + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "job": "Instance", + "major": "Major", + "minor": "Minor", + "patch": "Patch", + "version": "Version" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "rate(n8n_process_cpu_seconds_total{instance=~\"$instance\"}[5m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Total CPU", + "refId": "A" + }, + { + "expr": "rate(n8n_process_cpu_user_seconds_total{instance=~\"$instance\"}[5m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "User CPU", + "refId": "B" + }, + { + "expr": "rate(n8n_process_cpu_system_seconds_total{instance=~\"$instance\"}[5m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "System CPU", + "refId": "C" + } + ], + "title": "CPU Usage (5m rate)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decseconds" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_nodejs_eventloop_lag_seconds{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Current Lag", + "refId": "A" + }, + { + "expr": "n8n_nodejs_eventloop_lag_p50_seconds{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "p50", + "refId": "B" + }, + { + "expr": "n8n_nodejs_eventloop_lag_p99_seconds{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "p99", + "refId": "C" + }, + { + "expr": "n8n_nodejs_eventloop_lag_max_seconds{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max", + "refId": "D" + } + ], + "title": "Event Loop Lag", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Resident" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Heap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Virtual" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_process_resident_memory_bytes{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Resident", + "refId": "A" + }, + { + "expr": "n8n_process_heap_bytes{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Heap", + "refId": "B" + }, + { + "expr": "n8n_process_virtual_memory_bytes{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Virtual", + "refId": "C" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_nodejs_heap_size_used_bytes{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Used", + "refId": "A" + }, + { + "expr": "n8n_nodejs_heap_size_total_bytes{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Total", + "refId": "B" + }, + { + "expr": "n8n_nodejs_external_memory_bytes{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "External", + "refId": "C" + } + ], + "title": "Node.js Heap Memory", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 8, + "panels": [], + "title": "Heap Space Details", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_nodejs_heap_space_size_used_bytes{instance=~\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{space}}", + "refId": "A" + } + ], + "title": "Heap Space Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_nodejs_heap_space_size_available_bytes{instance=~\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{space}}", + "refId": "A" + } + ], + "title": "Heap Space Available", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 11, + "panels": [], + "title": "GC Statistics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 34 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "sum(rate(n8n_nodejs_gc_duration_seconds_sum{instance=~\"$instance\"}[5m])) by (kind)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{kind}}", + "refId": "A" + } + ], + "title": "GC Duration (5m rate)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 34 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "sum(rate(n8n_nodejs_gc_duration_seconds_count{instance=~\"$instance\"}[5m])) by (kind)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{kind}}", + "refId": "A" + } + ], + "title": "GC Count (5m rate)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 14, + "panels": [], + "title": "Active Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 43 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_nodejs_active_resources{instance=~\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Active Resources by Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 43 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_nodejs_active_handles{instance=~\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Active Handles by Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 43 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_process_open_fds{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open FDs", + "refId": "A" + }, + { + "expr": "n8n_process_max_fds{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max FDs", + "refId": "B" + } + ], + "title": "File Descriptors", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 18, + "panels": [], + "title": "Queue Statistics (Scaling Mode)", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 52 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_scaling_mode_queue_jobs_waiting{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Waiting Jobs", + "refId": "A" + } + ], + "title": "Queue Jobs Waiting", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 52 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_scaling_mode_queue_jobs_active{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Active Jobs", + "refId": "A" + } + ], + "title": "Queue Jobs Active", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 52 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_scaling_mode_queue_jobs_completed{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Completed Jobs", + "refId": "A" + }, + { + "expr": "n8n_scaling_mode_queue_jobs_failed{instance=~\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Failed Jobs", + "refId": "B" + } + ], + "title": "Queue Jobs Completed/Failed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 60 + }, + "id": 22, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "expr": "n8n_active_workflow_count{instance=~\"$instance\"}", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Active Workflows Count", + "type": "table" + } + ], + "preload": false, + "refresh": "30s", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "Prometheus", + "value": "PBFA97CFB590B2093" + }, + "label": "Prometheus", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "type": "datasource" + }, + { + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "definition": "label_values(n8n_version_info, instance)", + "includeAll": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(n8n_version_info, instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "n8n Monitoring", + "uid": "n8n-monitoring", + "version": 1 +} \ No newline at end of file diff --git a/grafana/provisioning/dashboards/main.yml b/grafana/provisioning/dashboards/main.yml new file mode 100644 index 0000000..94ae49a --- /dev/null +++ b/grafana/provisioning/dashboards/main.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "default" + orgId: 1 + folder: "" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards diff --git a/grafana/provisioning/datasources/main.yml b/grafana/provisioning/datasources/main.yml new file mode 100644 index 0000000..5798047 --- /dev/null +++ b/grafana/provisioning/datasources/main.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + jsonData: + timeInterval: 15s + editable: true diff --git a/n8n/backup/credentials/.gitkeep b/n8n/backup/credentials/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/n8n/backup/credentials/header_n8n_local.json b/n8n/backup/credentials/header_n8n_local.json new file mode 100644 index 0000000..d3e1cbc --- /dev/null +++ b/n8n/backup/credentials/header_n8n_local.json @@ -0,0 +1,16 @@ +{ + "updatedAt": "2026-04-11T02:55:18.014Z", + "createdAt": "2026-04-11T02:53:24.209Z", + "id": "VcZvOMLi8tCfxhei", + "name": "Header n8n local", + "data": { + "name": "X-N8N-API-KEY", + "value": "" + }, + "type": "httpHeaderAuth", + "isManaged": false, + "isGlobal": false, + "isResolvable": false, + "resolvableAllowFallback": false, + "resolverId": null +} \ No newline at end of file diff --git a/n8n/backup/credentials/n8n_local.json b/n8n/backup/credentials/n8n_local.json new file mode 100644 index 0000000..f5397fa --- /dev/null +++ b/n8n/backup/credentials/n8n_local.json @@ -0,0 +1,16 @@ +{ + "updatedAt": "2026-04-10T14:06:12.051Z", + "createdAt": "2026-04-10T13:58:36.675Z", + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local", + "data": { + "apiKey": "", + "baseUrl": "http://n8n:5678/api/v1" + }, + "type": "n8nApi", + "isManaged": false, + "isGlobal": false, + "isResolvable": false, + "resolvableAllowFallback": false, + "resolverId": null +} \ No newline at end of file diff --git a/n8n/backup/workflows/.gitkeep b/n8n/backup/workflows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/n8n/backup/workflows/Backup_Workflows.json b/n8n/backup/workflows/Backup_Workflows.json new file mode 100644 index 0000000..d6d15db --- /dev/null +++ b/n8n/backup/workflows/Backup_Workflows.json @@ -0,0 +1,182 @@ +[ + { + "updatedAt": "2026-03-06T10:45:21.272Z", + "createdAt": "2026-03-06T10:12:36.819Z", + "id": "bb4lGbKasex6fngs", + "name": "Backup Workflows", + "description": null, + "active": false, + "isArchived": false, + "nodes": [ + { + "parameters": { + "content": "## Экспортирует все workflows при помощи [CLI команд](https://docs.n8n.io/hosting/cli-commands/#workflows)\n\nПолучает список через локальный API, затем для каждого выполняет команду экспорта.\nФайлы сохраняются в /data/shared/workflows с именами процессов", + "height": 128, + "width": 800 + }, + "type": "n8n-nodes-base.stickyNote", + "position": [ + -256, + -240 + ], + "typeVersion": 1, + "id": "6e138476-8460-4d9d-9475-ba4fb5330c90", + "name": "Sticky Note" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -256, + -16 + ], + "id": "cdcd29a0-ea11-4a54-a2cc-3d673e463b14", + "name": "Запуск процесса" + }, + { + "parameters": { + "filters": {}, + "requestOptions": {} + }, + "type": "n8n-nodes-base.n8n", + "typeVersion": 1, + "position": [ + -48, + -16 + ], + "id": "5912c07d-3b77-43a9-bf8a-a438f4a751c5", + "name": "Получение всех workflows", + "credentials": { + "n8nApi": { + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local" + } + } + }, + { + "parameters": { + "command": "=mkdir -p /data/shared/workflows &&\nn8n export:workflow --pretty --id=\"{{ $json.id }}\" --output=\"/data/shared/workflows/{{ $json.name }}.json\"" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 400, + 0 + ], + "id": "edee2c90-217f-41b1-95f8-7b8f575c1d14", + "name": "Экспорт workflow в shared" + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 160, + -16 + ], + "id": "185e9856-ef99-46bd-8935-af5d75ef110a", + "name": "Для каждого workflow" + }, + { + "parameters": { + "content": "## Консольная команда [n8n export:workflow](https://docs.n8n.io/hosting/cli-commands/#workflows)\n| Flag | Описание |\n|------------|---------|\n| `--help` | Вывод справки |\n| `--all` | Экспортирует все workflows/credentials |\n| `--backup` | Устанавливает флаги `--all --pretty --separate` для создания резервной копии. Можно дополнительно указать `--output` |\n| `--id` | ID workflow для экспорта |\n| `--output` | Имя файла или директория (при использовании отдельных файлов) |\n| `--pretty` | Форматирует вывод для удобочитаемости |\n| `--separate` | Экспортирует по одному файлу на workflow (полезно для версионирования). Требует указания директории через `--output` |", + "height": 288, + "width": 816, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "position": [ + -272, + 256 + ], + "typeVersion": 1, + "id": "616d56ce-3144-4087-8d98-d9694dde5068", + "name": "Sticky Note1" + } + ], + "connections": { + "Запуск процесса": { + "main": [ + [ + { + "node": "Получение всех workflows", + "type": "main", + "index": 0 + } + ] + ] + }, + "Получение всех workflows": { + "main": [ + [ + { + "node": "Для каждого workflow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Экспорт workflow в shared": { + "main": [ + [ + { + "node": "Для каждого workflow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Для каждого workflow": { + "main": [ + [], + [ + { + "node": "Экспорт workflow в shared", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + }, + "staticData": null, + "meta": { + "templateCredsSetupCompleted": true + }, + "pinData": {}, + "versionId": "8cda3060-529d-4255-a977-7fd7fb12fb85", + "activeVersionId": null, + "versionCounter": 88, + "triggerCount": 0, + "tags": [], + "shared": [ + { + "updatedAt": "2026-03-06T10:12:36.819Z", + "createdAt": "2026-03-06T10:12:36.819Z", + "role": "workflow:owner", + "workflowId": "bb4lGbKasex6fngs", + "projectId": "cyhwgvy6YLlE2sea", + "project": { + "updatedAt": "2026-02-25T14:36:20.598Z", + "createdAt": "2026-02-25T14:34:43.191Z", + "id": "cyhwgvy6YLlE2sea", + "name": "Bolshakovsky Bolshakovsky ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "1e44e1f4-91c8-4fd2-96bc-df89e11c6414" + } + } + ] + } +] \ No newline at end of file diff --git a/n8n/backup/workflows/Backup_Сredentials.json b/n8n/backup/workflows/Backup_Сredentials.json new file mode 100644 index 0000000..4fc051b --- /dev/null +++ b/n8n/backup/workflows/Backup_Сredentials.json @@ -0,0 +1,120 @@ +[ + { + "updatedAt": "2026-03-06T10:45:17.431Z", + "createdAt": "2026-03-06T10:31:25.179Z", + "id": "8MvuleYK3IywiA5o", + "name": "Backup Сredentials", + "description": null, + "active": false, + "isArchived": false, + "nodes": [ + { + "parameters": { + "content": "## Экспортирует все credentials при помощи [CLI команд](https://docs.n8n.io/hosting/cli-commands/#credentials)\n\nФайлы сохраняются в /data/shared/credentials с id кредов", + "height": 128, + "width": 752 + }, + "type": "n8n-nodes-base.stickyNote", + "position": [ + -432, + -240 + ], + "typeVersion": 1, + "id": "a052c437-b5a5-4f65-ae95-63cb72753cf4", + "name": "Sticky Note" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -256, + -16 + ], + "id": "fb44d479-bc43-4b16-b1ae-8a7e9110d4d9", + "name": "Запуск процесса" + }, + { + "parameters": { + "command": "=mkdir -p /data/shared/credentials &&\nn8n export:credentials --all --backup --decrypted --output=/data/shared/credentials" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + -48, + -16 + ], + "id": "9bc3a040-32a8-4968-9a71-91487c07ed30", + "name": "Экспорт workflow в shared" + }, + { + "parameters": { + "content": "## Консольная команда [n8n export:credentials](https://docs.n8n.io/hosting/cli-commands/#credentials)\n| Flag | Описание |\n|------|----------|\n| `--help` | Вывод справки |\n| `--all` | Экспортирует все workflows/credentials |\n| `--backup` | Устанавливает флаги `--all --pretty --separate` для создания резервной копии. Можно дополнительно указать `--output` |\n| `--id` | ID workflow для экспорта |\n| `--output` | Имя файла или директория (при использовании отдельных файлов) |\n| `--pretty` | Форматирует вывод для удобочитаемости |\n| `--separate` | Экспортирует по одному файлу на workflow (полезно для версионирования). Требует указания директории через `--output` |\n| `--decrypted` | Экспортирует credentials в открытом текстовом формате |", + "height": 272, + "width": 752, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "position": [ + -416, + 192 + ], + "typeVersion": 1, + "id": "d238f978-acb3-46b9-afce-f056803ddf01", + "name": "Sticky Note1" + } + ], + "connections": { + "Запуск процесса": { + "main": [ + [ + { + "node": "Экспорт workflow в shared", + "type": "main", + "index": 0 + } + ] + ] + }, + "Экспорт workflow в shared": { + "main": [ + [] + ] + } + }, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate", + "availableInMCP": false + }, + "staticData": null, + "meta": { + "templateCredsSetupCompleted": true + }, + "pinData": {}, + "versionId": "915f573a-3ab0-4a4c-ba16-20b6f705bbc8", + "activeVersionId": null, + "versionCounter": 34, + "triggerCount": 0, + "tags": [], + "shared": [ + { + "updatedAt": "2026-03-06T10:31:25.179Z", + "createdAt": "2026-03-06T10:31:25.179Z", + "role": "workflow:owner", + "workflowId": "8MvuleYK3IywiA5o", + "projectId": "cyhwgvy6YLlE2sea", + "project": { + "updatedAt": "2026-02-25T14:36:20.598Z", + "createdAt": "2026-02-25T14:34:43.191Z", + "id": "cyhwgvy6YLlE2sea", + "name": "Bolshakovsky Bolshakovsky ", + "type": "personal", + "icon": null, + "description": null, + "creatorId": "1e44e1f4-91c8-4fd2-96bc-df89e11c6414" + } + } + ] + } +] \ No newline at end of file diff --git a/n8n/backup/workflows/Git_Commit_Workflows.json b/n8n/backup/workflows/Git_Commit_Workflows.json new file mode 100644 index 0000000..6467b67 --- /dev/null +++ b/n8n/backup/workflows/Git_Commit_Workflows.json @@ -0,0 +1,1105 @@ +{ + "name": "Git Commit Workflows", + "nodes": [ + { + "parameters": { + "content": "## Точка входа\n\nДва способа запустить workflow:\n\n**Форма** — основной способ. Пользователь заполняет данные, через форму\n\n**Ручной запуск** — для отладки.\n\n**Два режима фильтрации workflows:**\n- `Выгрузить все workflow` — `true` чтобы выгрузить всё, `false` для фильтрации по тегам\n- Список `Tags (через запятую)` — игнорируется если `Выгрузить все workflow: true`", + "height": 1036, + "width": 608, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 5152, + 176 + ], + "id": "d1421bd7-ad57-436f-93d9-fa72ee99b478", + "name": "Зона: Точка входа" + }, + { + "parameters": { + "content": "## Подготовка\n\nПарсим данные формы и строим все переменные необходимые для работы:\n- **remoteUrl** — URL с кредами вида `http://user:pat@host/org/repo.git`.\n- **cleanRemoteUrl** — URL без кредов.\n- **projectFolder** — локальная папка репозитория.\n- **commitMessage** — собирается из типа + текста коммита\n- **exportAll** — флаг «выгрузить все workflow». Если `true` — теги игнорируются\n- **tags** — список тегов (игнорируется при `exportAll = true`)\n- **skipWorkflows** — имена workflow которые всегда исключаются из выгрузки\n\n**Два режима сбора:**\n- `exportAll = true` → `Получить все workflow` (один запрос к n8n API)\n- `exportAll = false` → `Получить теги n8n` → `Фильтр тегов` → `Цикл по тегам` → `Получить workflow по тегу`\n\n⚠️Если ни один тег не найден и `exportAll = false` — бросаем ошибку.", + "height": 1036, + "width": 604, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 5808, + 176 + ], + "id": "d511af8a-d4eb-4b73-9dbc-dac6fd785a09", + "name": "Зона: Подготовка" + }, + { + "parameters": { + "content": "## Сбор workflow\n\nДва режима в зависимости от флага **Выгрузить все**:\n\n**Режим «Все workflow»:**\nОдин запрос к n8n API без фильтра — получаем полный список. Теги игнорируются.\n\n**Режим «По тегам»:**\nДля каждого совпавшего тега получаем список workflow через n8n API. Один workflow может иметь несколько тегов — после сбора применяем **дедупликацию** по `workflowId`.\n\nЕсли список пуст — показываем форму «Workflow не найдены».", + "height": 1036, + "width": 604, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 6464, + 176 + ], + "id": "a8cae61d-9f09-470e-bee5-af57075ae2f5", + "name": "Зона: Сбор workflow" + }, + { + "parameters": { + "content": "## Git Clone / Pull и подготовка репозитория\n\nВыполняется **один раз** перед началом итерации по workflow.\n\nЧто происходит:\n1. Создаём родительскую папку (`exportFolder/org/`) если её нет\n2. **Если папка уже есть** (`projectFolder/.git`):\n - Обновляем remote URL (с кредами текущего пользователя)\n - `git fetch` — получаем актуальные ветки\n - Переключаемся на нужную ветку\n - `git reset --hard origin/branch` — приводим к состоянию remote\n3. **Если папки нет** — `git clone` нужной ветки (или создаём новую если её нет на remote)\nПапка **никогда не удаляется** — каждый запуск только актуализирует.\n\n⚠️ Если нет git `user.email` и `user.name` - настраиваются автоматически", + "height": 1036, + "width": 604, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 7120, + 176 + ], + "id": "252d213a-25d6-43a9-83d5-f7006d445119", + "name": "Зона: Git Init" + }, + { + "parameters": { + "content": "## Синхронизация и экспорт файлов\n\n**Синхронизация удалённых workflow:**\nПеред записью сравниваем список `.json` файлов в папке с актуальным списком workflow. Файлы которых больше нет среди собранных workflow — **удаляются**.\n\n**Для каждого workflow из цикла:**\n\n**Записать файл** (`executeCommand`) — экспортируем через CLI прямо в целевой файл:\n`n8n export:workflow --id= --output=`\n\nCLI сам создаёт или перезаписывает файл. Проверка изменений не выполняется — git определит diff при коммите.", + "height": 1028, + "width": 604, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 7776, + 176 + ], + "id": "a93df50a-5755-4a8d-a262-748f89cdd158", + "name": "Зона: Экспорт файлов" + }, + { + "parameters": { + "content": "## Коммит, пуш и очистка\n\nПосле того как все файлы экспортированы (цикл завершён — выход 0):\n\n1. **Проверить изменения git** — `git add -A` + `git diff --cached HEAD`:\n - `HAS_CHANGES` → делаем коммит\n - `NO_CHANGES` → показываем форму «Нет изменений»\n\n2. **Git Commit** — `git commit -m \"commitMessage\"`\n\n3. **Git Push** — `git push origin branch || git push --set-upstream origin branch`. Для новой ветки автоматически добавляет `--set-upstream`.\n\n4. **Успешный Push?** (IF) — проверяем stdout на наличие `fatal:` или `error:` (AND):\n - Нет ошибок → **Очистить Remote от кредов** → **Форма: Успех**\n - Есть ошибки → **Форма: Ошибка push**\n\n5. **Очистить Remote от кредов**\n\n⚠️ ВАЖНО! Заменяем remote URL на версию БЕЗ кредов. Защищает от случайного использования чужого PAT при следующем запуске.\n\n⚠️ Локальная папка **не удаляется** — остаётся для следующего запуска.", + "height": 1028, + "width": 1356, + "color": 3 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 8448, + 176 + ], + "id": "92723155-adb4-4a8d-92ec-2e7dc0ce7d38", + "name": "Зона: Коммит и пуш" + }, + { + "parameters": { + "formTitle": "Экспорт Workflow в Git", + "formDescription": "Заполните данные для коммита workflow в репозиторий", + "formFields": { + "values": [ + { + "fieldLabel": "Username", + "placeholder": "gitea_username", + "requiredField": true + }, + { + "fieldLabel": "Personal Access Token", + "fieldType": "password", + "placeholder": "your_personal_access_token", + "requiredField": true + }, + { + "fieldLabel": "Repository URL", + "placeholder": "https://", + "defaultValue": "https://vcs.bbr.ru", + "requiredField": true + }, + { + "fieldLabel": "Organization", + "placeholder": "Название организации", + "requiredField": true + }, + { + "fieldLabel": "Repository Name", + "placeholder": "my-project", + "defaultValue": "workflows", + "requiredField": true + }, + { + "fieldLabel": "Branch Type", + "fieldType": "dropdown", + "defaultValue": "main", + "fieldOptions": { + "values": [ + { + "option": "main" + }, + { + "option": "develop" + }, + { + "option": "release/" + }, + { + "option": "feature/" + }, + { + "option": "hotfix/" + }, + { + "option": "custom" + } + ] + }, + "requiredField": true + }, + { + "fieldLabel": "Custom Branch Name", + "placeholder": "Для release/feature/hotfix укажите суффикс или полное имя для custom" + }, + { + "fieldLabel": "Commit Type", + "fieldType": "dropdown", + "defaultValue": "feat: Новая функциональность", + "fieldOptions": { + "values": [ + { + "option": "feat: Новая функциональность" + }, + { + "option": "fix: Исправление ошибки" + }, + { + "option": "docs: Документация" + }, + { + "option": "style: Форматирование" + }, + { + "option": "refactor: Рефакторинг" + }, + { + "option": "test: Тесты" + }, + { + "option": "chore: Служебные изменения" + }, + { + "option": "perf: Производительность" + }, + { + "option": "ci: CI/CD" + } + ] + }, + "requiredField": true + }, + { + "fieldLabel": "Commit Message", + "placeholder": "Описание изменений", + "requiredField": true + }, + { + "fieldLabel": "Выгрузить все workflow", + "fieldType": "radio", + "defaultValue": "0", + "fieldOptions": { + "values": [ + { + "option": "Да" + } + ] + } + }, + { + "fieldLabel": "Tags (через запятую)", + "placeholder": "production,finance,crm", + "defaultValue": "metric", + "requiredField": true + }, + { + "fieldLabel": "Export Folder", + "placeholder": "/data/shared", + "defaultValue": "/data/shared", + "requiredField": true + } + ] + }, + "responseMode": "lastNode", + "options": { + "path": "154391ab-386d-42ce-8074-19a28d05544b", + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "id": "cfe3d2d1-e962-46b0-bd23-7cc9873ce956", + "name": "Форма запроса", + "type": "n8n-nodes-base.formTrigger", + "typeVersion": 2.2, + "position": [ + 5328, + 672 + ], + "webhookId": "154391ab-386d-42ce-8074-19a28d05544b" + }, + { + "parameters": { + "jsCode": "const body = $input.first().json;\n\nlet branch;\nconst branchType = body['Branch Type'];\nconst customBranch = (body['Custom Branch Name'] || '').trim();\n\nif (branchType === 'custom') {\n if (!customBranch) throw new Error('Custom Branch Name обязателен при выборе custom');\n branch = customBranch;\n} else if (['release/', 'feature/', 'hotfix/'].includes(branchType)) {\n if (!customBranch) throw new Error(`Укажите суффикс для ветки ${branchType}`);\n branch = branchType + customBranch;\n} else {\n branch = branchType;\n}\n\nconst commitTypeRaw = body['Commit Type'];\nconst commitTypePrefix = commitTypeRaw.split(':')[0].trim();\nconst commitMsg = (body['Commit Message'] || '').trim();\nconst commitMessage = `${commitTypePrefix}: ${commitMsg}`;\n\nconst username = (body['Username']).trim();\nconst pat = (body['Personal Access Token']).trim();\nconst org = (body['Organization'] || '').trim();\nconst repoUrl = (body['Repository URL']).trim().replace(/\\/$/, '');\nconst repoName = (body['Repository Name']).trim();\nconst exportFolder = (body['Export Folder'] || '/data/shared').trim().replace(/\\/$/, '');\n\nconst authUrl = repoUrl.replace(/^(https?:\\/\\/)/, `$1${encodeURIComponent(username)}:${encodeURIComponent(pat)}@`);\nconst remoteUrl = `${authUrl}/${org}/${repoName}.git`;\nconst cleanRemoteUrl = `${repoUrl}/${org}/${repoName}.git`;\n\nconst projectFolder = `${exportFolder}/${org}/${repoName}`;\n\nconst exportAll = !!(body['Выгрузить все workflow']);\nconst tags = (body['Tags (через запятую)'] || '').split(',').map(t => t.trim()).filter(Boolean);\nif (!exportAll && tags.length === 0) throw new Error('Укажите хотя бы один тег или включите \"Выгрузить все workflow\"');\nconst skipWorkflows = [\n 'Git Commit Workflows',\n 'Git Pull Workflows',\n 'Backup Workflows',\n 'Backup Сredentials',\n];\n\n\nreturn [{\n json: {\n username,\n pat,\n repoUrl,\n repoName,\n org,\n remoteUrl,\n cleanRemoteUrl,\n branch,\n commitMessage,\n exportAll,\n tags,\n skipWorkflows,\n projectFolder,\n exportFolder,\n }\n}];" + }, + "id": "887f1e0f-1b6c-47b7-9644-75e388ece60c", + "name": "Подготовка переменных", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 5888, + 672 + ] + }, + { + "parameters": { + "url": "http://n8n:5678/api/v1/tags", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": {} + }, + "id": "c3de3ce6-7e43-4790-9091-686628db6874", + "name": "Получить теги n8n", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 6064, + 672 + ], + "credentials": { + "httpHeaderAuth": { + "id": "VcZvOMLi8tCfxhei", + "name": "Header n8n local" + } + } + }, + { + "parameters": { + "jsCode": "const tagsResponse = $input.first().json;\nconst allTags = tagsResponse.data || [];\nconst ctx = $('Подготовка переменных').first().json;\nconst requestedTags = ctx.tags;\n\nif (!requestedTags || requestedTags.length === 0) {\n return [{ json: {} }];\n}\n\nconst matched = allTags.filter(tag => requestedTags.includes(tag.name));\nconst notFound = requestedTags.filter(t => !matched.find(m => m.name === t));\n\nif (notFound.length > 0) {\n console.log(`Предупреждение: теги не найдены: ${notFound.join(', ')}`);\n}\n\nif (matched.length === 0) {\n return [{ json: {} }];\n}\n\nreturn matched.map(tag => ({\n json: {\n matchedTagId: tag.id,\n matchedTagName: tag.name,\n }\n}));" + }, + "id": "7d93f7fd-a2f3-42dc-8b30-0adef4f8d75d", + "name": "Фильтр тегов", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6240, + 672 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "d240ec1f-37e8-4d7f-bb7b-af498d499847", + "name": "Цикл по тегам", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 6544, + 672 + ] + }, + { + "parameters": { + "filters": { + "tags": "={{ $json.matchedTagName }}" + }, + "requestOptions": {} + }, + "type": "n8n-nodes-base.n8n", + "typeVersion": 1, + "position": [ + 6544, + 896 + ], + "id": "88466772-d7d2-4270-a523-db27c82436ae", + "name": "Получить workflow по тегу", + "alwaysOutputData": true, + "credentials": { + "n8nApi": { + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local" + } + } + }, + { + "parameters": { + "jsCode": "const variables = $('Подготовка переменных').first().json;\nconst projectFolder = variables.projectFolder;\nconst skipWorkflows = variables.skipWorkflows || [];\n\nconst result = $input.all()\n .filter(item => item.json && item.json.name)\n .filter(item => !skipWorkflows.includes(item.json.name))\n .map(item => {\n const workflow = item.json;\n\n const safeName = workflow.name\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .trim();\n\n const filePath = `${projectFolder}/${safeName}.json`;\n\n return {\n json: {\n workflowId: workflow.id,\n workflowName: workflow.name,\n filePath,\n projectFolder,\n }\n };\n });\n\nif (result.length === 0) {\n $execution.customData.set('empty', 'true');\n return [{ json: { empty: true } }];\n}\n\nreturn result;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6736, + 896 + ], + "id": "c73e0b04-866a-4007-ad7c-07d2693c87b9", + "name": "Подготовить пути файлов" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "3db8353a-8212-44b5-b5cd-b172abc862ad", + "leftValue": "={{ $input.all() }}", + "rightValue": "", + "operator": { + "type": "array", + "operation": "notEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 6736, + 672 + ], + "id": "f32586e3-2e1f-4899-9cd8-4e1b2a9aba80", + "name": "Есть workflow?" + }, + { + "parameters": { + "compare": "selectedFields", + "fieldsToCompare": "workflowId", + "options": {} + }, + "type": "n8n-nodes-base.removeDuplicates", + "typeVersion": 2, + "position": [ + 7184, + 672 + ], + "id": "a16c52a5-e26d-496c-8be6-b820c84dae85", + "name": "Удалить дубликаты" + }, + { + "parameters": { + "command": "=PROJECT_FOLDER=\"{{ $('Подготовка переменных').first().json.projectFolder }}\"\nREMOTE_URL=\"{{ $('Подготовка переменных').first().json.remoteUrl }}\"\nBRANCH=\"{{ $('Подготовка переменных').first().json.branch }}\"\n\nmkdir -p \"$(dirname \"$PROJECT_FOLDER\")\"\n\nif [ -d \"$PROJECT_FOLDER/.git\" ]; then\n cd \"$PROJECT_FOLDER\"\n git remote set-url origin \"$REMOTE_URL\"\n git fetch origin 2>/dev/null || true\n BRANCH_REMOTE=$(git branch -r --list \"origin/$BRANCH\")\n if git branch --list \"$BRANCH\" | grep -q \"$BRANCH\"; then\n git checkout \"$BRANCH\"\n if [ -n \"$BRANCH_REMOTE\" ]; then\n git reset --hard \"origin/$BRANCH\"\n fi\n elif [ -n \"$BRANCH_REMOTE\" ]; then\n git checkout -b \"$BRANCH\" \"origin/$BRANCH\"\n else\n git checkout -b \"$BRANCH\"\n fi\nelse\n BRANCH_REMOTE_CHECK=$(git ls-remote --heads \"$REMOTE_URL\" \"$BRANCH\" 2>/dev/null | wc -l)\n if [ \"$BRANCH_REMOTE_CHECK\" -gt \"0\" ]; then\n git clone --branch \"$BRANCH\" \"$REMOTE_URL\" \"$PROJECT_FOLDER\"\n cd \"$PROJECT_FOLDER\"\n else\n git clone \"$REMOTE_URL\" \"$PROJECT_FOLDER\" 2>/dev/null || git init \"$PROJECT_FOLDER\"\n cd \"$PROJECT_FOLDER\"\n git remote add origin \"$REMOTE_URL\" 2>/dev/null || git remote set-url origin \"$REMOTE_URL\"\n git checkout -b \"$BRANCH\" 2>/dev/null || git checkout \"$BRANCH\"\n fi\nfi\n\ncd \"$PROJECT_FOLDER\"\ngit config user.email \"n8n@automation.local\"\ngit config user.name \"n8n Automation\"\n\necho \"READY: $BRANCH | $PROJECT_FOLDER\"" + }, + "id": "449d595a-321d-4f16-a8f6-51ca21a0146b", + "name": "Git Init / Переключить ветку", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 7376, + 672 + ] + }, + { + "parameters": { + "mode": "combine", + "combineBy": "combineAll", + "options": {} + }, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2, + "position": [ + 7568, + 672 + ], + "id": "309246c1-b4eb-47c9-84ca-feabd2ecabd4", + "name": "Ожидать Git Init" + }, + { + "parameters": { + "options": {} + }, + "id": "b6fb588d-40ae-4b47-935a-a170aeed1e96", + "name": "Цикл по workflow", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 8032, + 672 + ] + }, + { + "parameters": { + "command": "=mkdir -p \"{{ $json.projectFolder }}\"\nn8n export:workflow --id=\"{{ $json.workflowId }}\" --pretty --output=\"{{ $json.filePath }}\"\necho \"EXPORTED: {{ $json.filePath }}\"" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 8208, + 896 + ], + "id": "628246e2-0dfc-4558-a048-9cfb2c420bbe", + "name": "Записать файл" + }, + { + "parameters": { + "command": "=cd \"{{ $('Подготовка переменных').first().json.projectFolder }}\"\n\ngit add -A\n\n# Сравниваем с последним коммитом на текущей ветке\n# Если коммитов нет (новая ветка) — считаем что всё новое\nif git rev-parse HEAD 2>/dev/null; then\n if git diff --cached HEAD --quiet; then\n echo \"NO_CHANGES\"\n else\n echo \"HAS_CHANGES\"\n fi\nelse\n # Нет коммитов — новая ветка, всё новое\n if git diff --cached --quiet; then\n echo \"NO_CHANGES\"\n else\n echo \"HAS_CHANGES\"\n fi\nfi" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 8496, + 672 + ], + "id": "3a621797-0ce2-426e-b789-b5049c656583", + "name": "Проверить изменения git" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "4673d9eb-d1ff-42e0-a42d-fa377b99dd93", + "leftValue": "={{ $json.stdout.trim() }}", + "rightValue": "HAS_CHANGES", + "operator": { + "type": "string", + "operation": "contains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 8688, + 672 + ], + "id": "9831a213-3ccd-4d14-bbea-82a38083ca0b", + "name": "Есть изменения?" + }, + { + "parameters": { + "command": "=cd \"{{ $('Подготовка переменных').first().json.projectFolder }}\"\nCOMMIT_MSG=\"{{ $('Подготовка переменных').first().json.commitMessage }}\"\n\ngit add -A\n\nif git diff --cached --quiet; then\n echo \"NO_CHANGES\"\nelse\n git commit -m \"$COMMIT_MSG\"\n echo \"COMMITTED\"\nfi" + }, + "id": "1b6c77fa-fbe7-4eba-8a27-da23b2bbdf5b", + "name": "Git Commit", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 8896, + 672 + ] + }, + { + "parameters": { + "command": "=cd \"{{ $('Подготовка переменных').first().json.projectFolder }}\"\nBRANCH=\"{{ $('Подготовка переменных').first().json.branch }}\"\ngit push origin \"$BRANCH\" 2>&1 || git push --set-upstream origin \"$BRANCH\" 2>&1 || true" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 9072, + 672 + ], + "id": "b8fc50b7-4315-4c9d-8c53-ebfacf3375b3", + "name": "Git Push" + }, + { + "parameters": { + "command": "=cd \"{{ $('Подготовка переменных').first().json.projectFolder }}\"\nCLEAN_URL=\"{{ $('Подготовка переменных').first().json.cleanRemoteUrl }}\"\n\ngit remote set-url origin \"$CLEAN_URL\"\n\necho \"REMOTE_CLEANED: $CLEAN_URL\"" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 9440, + 672 + ], + "id": "50523ba5-067b-46da-beb6-dee37d34d9cf", + "name": "Очистить Remote от кредов" + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Workflow не найдены", + "completionMessage": "По указанным тегам workflow не найдено. Проверьте теги и попробуйте снова", + "limitWaitTime": true, + "resumeUnit": "minutes", + "options": { + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "type": "n8n-nodes-base.form", + "typeVersion": 2.5, + "position": [ + 6928, + 896 + ], + "id": "28066c34-7c3f-4c2a-ad69-7f3ee1fbd6d0", + "name": "Форма: Workflow не найдены", + "webhookId": "6f77d74e-f7a4-443e-a8c7-7490cecf8ca9" + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Всё актуально", + "completionMessage": "Workflow не изменились с последнего коммита", + "limitWaitTime": true, + "resumeUnit": "minutes", + "options": { + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "type": "n8n-nodes-base.form", + "typeVersion": 2.5, + "position": [ + 8896, + 896 + ], + "id": "e6a68407-6ccc-447d-bc68-b8f6785704b3", + "name": "Форма: Нет изменений", + "webhookId": "b2c3d4e5-f6a7-8901-bcde-f01234567890" + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Готово", + "completionMessage": "=Workflow успешно экспортированы и отправлены в репозиторий\n\nВетка: {{ $('Подготовка переменных').first().json.branch }}\nРепозиторий: {{ $('Подготовка переменных').first().json.org }}/{{ $('Подготовка переменных').first().json.repoName }}\nКоммит: {{ $('Подготовка переменных').first().json.commitMessage }}\n", + "limitWaitTime": true, + "resumeUnit": "minutes", + "options": { + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "type": "n8n-nodes-base.form", + "typeVersion": 2.5, + "position": [ + 9616, + 672 + ], + "id": "133dd868-3197-4ac9-a0b9-2f2ca41b9521", + "name": "Форма: Успех", + "webhookId": "c3d4e5f6-a7b8-9012-cdef-012345678901" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 5328, + 896 + ], + "id": "f8fe696e-8b61-4321-865d-4c9b2b8b30f1", + "name": "Ручной запуск (отладка)" + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "{\n \"Username\": \"gitea\",\n \"Personal Access Token\": \"0f276ee359b823464680037cb9b62f3558796f95\",\n \"Repository URL\": \"http://192.168.1.155:3001\",\n \"Repository Name\": \"workflows\",\n \"Organization\": \"event_forge\",\n \"Branch Type\": \"main\",\n \"Custom Branch Name\": \"\",\n \"Commit Type\": \"feat: Новая функциональность\",\n \"Commit Message\": \"Тестовый коммит\",\n \"Выгрузить все workflow\": false,\n \"Tags (через запятую)\": \"metric,test-git\",\n \"Export Folder\": \"/data/shared\",\n \"submittedAt\": \"2026-04-09T17:24:20.346+03:00\",\n \"formMode\": \"test\"\n}", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 5520, + 896 + ], + "id": "43ee0bee-f5ea-4841-b840-674a12ed31c6", + "name": "Тестовые данные" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst path = require('path');\n\nconst variables = $('Подготовка переменных').first().json;\nconst projectFolder = variables.projectFolder;\n\n// Собираем имена файлов которые должны быть\nconst expectedFiles = new Set(\n $input.all()\n .filter(i => i.json && i.json.filePath)\n .map(i => path.basename(i.json.filePath))\n);\n\n// Удаляем лишние файлы (workflow были удалены в n8n)\nlet deleted = [];\nif (fs.existsSync(projectFolder)) {\n const existing = fs.readdirSync(projectFolder).filter(f => f.endsWith('.json'));\n for (const file of existing) {\n if (!expectedFiles.has(file)) {\n fs.unlinkSync(path.join(projectFolder, file));\n deleted.push(file);\n console.log(`DELETED (removed workflow): ${file}`);\n }\n }\n}\n\nconsole.log(`Sync: expected=${expectedFiles.size}, deleted=${deleted.length}`);\nreturn $input.all();" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7840, + 672 + ], + "id": "90ae57be-52a6-400e-9dc1-80a171e2d381", + "name": "Синхронизировать удалённые файлы" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "b1c2d3e4-f5a6-7890-abcd-123456789abc", + "leftValue": "={{ $('Подготовка переменных').item.json.exportAll }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 5888, + 896 + ], + "id": "bdca851d-c812-480b-894e-fb2a81a7b440", + "name": "Выгрузить все?" + }, + { + "parameters": { + "filters": {}, + "requestOptions": {} + }, + "type": "n8n-nodes-base.n8n", + "typeVersion": 1, + "position": [ + 6064, + 896 + ], + "id": "fe828c0d-cad0-4137-a4bc-e3b5a2e6ac30", + "name": "Получить все workflow", + "alwaysOutputData": true, + "credentials": { + "n8nApi": { + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local" + } + } + }, + { + "parameters": { + "jsCode": "const variables = $('Подготовка переменных').first().json;\nconst projectFolder = variables.projectFolder;\nconst skipWorkflows = variables.skipWorkflows || [];\n\nconst result = $input.all()\n .filter(item => item.json && item.json.name)\n .filter(item => !skipWorkflows.includes(item.json.name))\n .map(item => {\n const workflow = item.json;\n\n const safeName = workflow.name\n .replace(/[<>:\"/\\\\|?*]/g, '')\n .replace(/\\s+/g, '_')\n .trim();\n\n const filePath = `${projectFolder}/${safeName}.json`;\n\n return {\n json: {\n workflowId: workflow.id,\n workflowName: workflow.name,\n filePath,\n projectFolder,\n }\n };\n });\n\nif (result.length === 0) {\n $execution.customData.set('empty', 'true');\n return [{ json: { empty: true } }];\n}\n\nreturn result;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6240, + 896 + ], + "id": "08150d3b-066f-4dc1-ba57-221475c5fe63", + "name": "Подготовить пути (все)" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "f22669db-3bcc-4732-9506-b90b63a81ee3", + "leftValue": "={{ $json.stdout }}", + "rightValue": "fatal", + "operator": { + "type": "string", + "operation": "notContains" + } + }, + { + "id": "ceeb9c23-47a8-4d5e-9ddb-0960fe4f59e4", + "leftValue": "={{ $json.stdout }}", + "rightValue": "error", + "operator": { + "type": "string", + "operation": "notContains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 9248, + 672 + ], + "id": "cef87e93-feac-4ac2-9f97-8e606fb8be15", + "name": "Успешный Push?" + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Ошибка push", + "completionMessage": "=Произошла ошибка при push: {{ $json.stdout }}", + "limitWaitTime": true, + "resumeUnit": "minutes", + "options": { + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "type": "n8n-nodes-base.form", + "typeVersion": 2.5, + "position": [ + 9440, + 896 + ], + "id": "edec2ba9-6272-43b4-be06-51b2f47ed273", + "name": "Форма: Ошибка push", + "webhookId": "b2c3d4e5-f6a7-8901-bcde-f01234567890" + } + ], + "pinData": {}, + "connections": { + "Форма запроса": { + "main": [ + [ + { + "node": "Подготовка переменных", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ручной запуск (отладка)": { + "main": [ + [ + { + "node": "Тестовые данные", + "type": "main", + "index": 0 + } + ] + ] + }, + "Тестовые данные": { + "main": [ + [ + { + "node": "Подготовка переменных", + "type": "main", + "index": 0 + } + ] + ] + }, + "Подготовка переменных": { + "main": [ + [ + { + "node": "Получить теги n8n", + "type": "main", + "index": 0 + } + ] + ] + }, + "Получить теги n8n": { + "main": [ + [ + { + "node": "Выгрузить все?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Фильтр тегов": { + "main": [ + [ + { + "node": "Цикл по тегам", + "type": "main", + "index": 0 + } + ] + ] + }, + "Цикл по тегам": { + "main": [ + [ + { + "node": "Есть workflow?", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Получить workflow по тегу", + "type": "main", + "index": 0 + } + ] + ] + }, + "Получить workflow по тегу": { + "main": [ + [ + { + "node": "Подготовить пути файлов", + "type": "main", + "index": 0 + } + ] + ] + }, + "Подготовить пути файлов": { + "main": [ + [ + { + "node": "Цикл по тегам", + "type": "main", + "index": 0 + } + ] + ] + }, + "Есть workflow?": { + "main": [ + [ + { + "node": "Удалить дубликаты", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Форма: Workflow не найдены", + "type": "main", + "index": 0 + } + ] + ] + }, + "Удалить дубликаты": { + "main": [ + [ + { + "node": "Ожидать Git Init", + "type": "main", + "index": 1 + }, + { + "node": "Git Init / Переключить ветку", + "type": "main", + "index": 0 + } + ] + ] + }, + "Git Init / Переключить ветку": { + "main": [ + [ + { + "node": "Ожидать Git Init", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ожидать Git Init": { + "main": [ + [ + { + "node": "Синхронизировать удалённые файлы", + "type": "main", + "index": 0 + } + ] + ] + }, + "Цикл по workflow": { + "main": [ + [ + { + "node": "Проверить изменения git", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Записать файл", + "type": "main", + "index": 0 + } + ] + ] + }, + "Записать файл": { + "main": [ + [ + { + "node": "Цикл по workflow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Проверить изменения git": { + "main": [ + [ + { + "node": "Есть изменения?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Есть изменения?": { + "main": [ + [ + { + "node": "Git Commit", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Форма: Нет изменений", + "type": "main", + "index": 0 + } + ] + ] + }, + "Git Commit": { + "main": [ + [ + { + "node": "Git Push", + "type": "main", + "index": 0 + } + ] + ] + }, + "Git Push": { + "main": [ + [ + { + "node": "Успешный Push?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Очистить Remote от кредов": { + "main": [ + [ + { + "node": "Форма: Успех", + "type": "main", + "index": 0 + } + ] + ] + }, + "Синхронизировать удалённые файлы": { + "main": [ + [ + { + "node": "Цикл по workflow", + "type": "main", + "index": 0 + } + ] + ] + }, + "Выгрузить все?": { + "main": [ + [ + { + "node": "Получить все workflow", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Фильтр тегов", + "type": "main", + "index": 0 + } + ] + ] + }, + "Получить все workflow": { + "main": [ + [ + { + "node": "Подготовить пути (все)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Подготовить пути (все)": { + "main": [ + [ + { + "node": "Есть workflow?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Успешный Push?": { + "main": [ + [ + { + "node": "Очистить Remote от кредов", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Форма: Ошибка push", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "c3caa6fa-12a3-414f-91c3-74497aba7106", + "meta": { + "instanceId": "e5d2d5dcf3b84baf4a3229099b56ef256963029b1ff1917093a2e26870e31e8f" + }, + "id": "1K7zE39Cw01WwP8O", + "tags": [] +} \ No newline at end of file diff --git a/n8n/backup/workflows/Git_Pull_Workflows.json b/n8n/backup/workflows/Git_Pull_Workflows.json new file mode 100644 index 0000000..c15cbd8 --- /dev/null +++ b/n8n/backup/workflows/Git_Pull_Workflows.json @@ -0,0 +1,854 @@ +{ + "name": "Git Pull Workflows", + "nodes": [ + { + "parameters": { + "content": "## Точка входа\n\nДва способа запустить workflow:\n\n**Форма** — основной способ. Пользователь заполняет данные репозитория, ветку, папку и переключатель **Активировать workflow** — разрешить ли активацию вообще.\n\n**Ручной запуск** — для отладки без открытия формы. Тестовые данные задаются в ноде «Тестовые данные»", + "height": 964, + "width": 588, + "color": 7 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 5136, + 496 + ], + "id": "987b6189-d7c7-4b68-86cf-b568c1f92096", + "name": "Зона: Точка входа" + }, + { + "parameters": { + "content": "## Подготовка и git операции\n\nПарсим данные формы и строим переменные:\n- **remoteUrl** — URL с кредами вида `http://user:pat@host/org/repo.git`. Используется только для git операций\n- **cleanRemoteUrl** — URL без кредов\n- **localFolder** — локальная папка репозитория\n\n**Git Clone / Pull:**\n- Папки нет или нет `.git` -> `git clone --branch`\n- Папка есть → обновляем remote URL, `git fetch`, `git checkout`, `git pull`\n\n⚠️ После операций сразу очищаем remote от кредов\n\n**Pull успешен?** — проверяем `exitCode === 0`. При ошибке показываем форму с текстом из stderr.", + "height": 964, + "width": 600, + "color": 5 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 5776, + 496 + ], + "id": "523cc286-1ae6-4647-bbe3-f36e3164a3b1", + "name": "Зона: Подготовка и git pull" + }, + { + "parameters": { + "content": "## Чтение файлов и определение статуса\n\nПолучаем список всех workflow из n8n через API, затем читаем все `.json` файлы из локальной папки репозитория.\n\nДля каждого файла определяем статус:\n- **NEW** — workflow с таким именем не найден в n8n\n- **CHANGED** — найден, но `updatedAt` отличается\n- **UNCHANGED** — найден и `updatedAt` совпадает — пропускаем\n\nДополнительно читаем поле **`active`** из JSON файла — оно определяет нужно ли активировать workflow после импорта (если разрешено глобально).\n\nФайлы которые не удалось распарсить пропускаются с выводом в console.log.", + "height": 964, + "width": 600, + "color": 6 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 6432, + 496 + ], + "id": "6f3247a2-d433-4f75-ba92-832b33e5a584", + "name": "Зона: Чтение файлов" + }, + { + "parameters": { + "content": "## Импорт через CLI\n\nДля каждого файла:\n\n**Switch — маршрутизация по статусу:**\n- `NEW` → импортируем через `n8n import:workflow`\n- `CHANGED` → импортируем через `n8n import:workflow`\n- `UNCHANGED` → пропускаем, сразу пишем результат\n\n**Разбор результата импорта** — парсим stdout, извлекаем статус успеха.\n\n**Нужно активировать?** — IF нода, все три условия должны выполняться:\n1. `activate = true` — пользователь разрешил активацию в форме\n2. `wfActive = true` — в JSON файле workflow был `\"active\": true`\n3. `success = true` — импорт прошёл без ошибок\n\n- Да → **Активировать workflow** (`n8n update:workflow --id= --active=true`)\n- Нет → сразу **Записать результат**", + "height": 964, + "width": 1040, + "color": 4 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 7088, + 496 + ], + "id": "e108e18e-20b7-4a4c-b6dd-c21c7ff13b52", + "name": "Зона: Поиск и импорт" + }, + { + "parameters": { + "content": "## Итоговый отчёт\n\nПосле завершения цикла собираем статистику по всем файлам:\n- ✅ **Создано** — NEW + imported\n- 🔄 **Обновлено** — CHANGED + imported\n- ➖ **Пропущено** — UNCHANGED\n- ❌ **Ошибок** — не импортировались\n- ▶️ **Активировано** — workflow у которых `active: true` в файле и пользователь разрешил активацию\n\nВ детальном списке каждый активированный workflow помечается ▶️.\n\n⚠️ После импорта обязательно проверьте:\n1. Credentials в нодах — нужно перепривязать вручную\n2. Webhook URL — могли измениться", + "height": 964, + "width": 592, + "color": 3 + }, + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 8192, + 496 + ], + "id": "8fc98c91-a6ab-42a3-af2f-b4f78b7b31af", + "name": "Зона: Результат" + }, + { + "parameters": { + "formTitle": "Импорт Workflow из Git", + "formDescription": "Заполните данные для загрузки workflow из репозитория в n8n", + "formFields": { + "values": [ + { + "fieldLabel": "Username", + "placeholder": "gitea_username", + "requiredField": true + }, + { + "fieldLabel": "Personal Access Token", + "fieldType": "password", + "placeholder": "your_personal_access_token", + "requiredField": true + }, + { + "fieldLabel": "Repository URL", + "placeholder": "https://", + "defaultValue": "https://vcs.bbr.ru", + "requiredField": true + }, + { + "fieldLabel": "Organization", + "placeholder": "Название организации", + "defaultValue": "event_forge", + "requiredField": true + }, + { + "fieldLabel": "Repository Name", + "placeholder": "my-project", + "defaultValue": "workflows", + "requiredField": true + }, + { + "fieldLabel": "Branch", + "fieldType": "dropdown", + "defaultValue": "main", + "fieldOptions": { + "values": [ + { + "option": "main" + }, + { + "option": "develop" + }, + { + "option": "custom" + } + ] + }, + "requiredField": true + }, + { + "fieldLabel": "Custom Branch Name", + "placeholder": "Полное имя ветки если выбран custom" + }, + { + "fieldLabel": "Export Folder", + "placeholder": "/data/shared", + "defaultValue": "/data/shared", + "requiredField": true + }, + { + "fieldLabel": "Активировать workflow", + "fieldType": "radio", + "defaultValue": "0", + "fieldOptions": { + "values": [ + { + "option": "Да" + } + ] + } + } + ] + }, + "responseMode": "lastNode", + "options": { + "path": "99ac2ec1-b958-4522-99f3-136a8f080ed1", + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "id": "c6aa7731-f958-4895-93e0-99960dab73e9", + "name": "Форма запроса", + "type": "n8n-nodes-base.formTrigger", + "typeVersion": 2.2, + "position": [ + 5280, + 912 + ], + "webhookId": "99ac2ec1-b958-4522-99f3-136a8f080ed1" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 5280, + 1136 + ], + "id": "0bb92efd-de10-4f1a-9372-9f41e9b74b8e", + "name": "Ручной запуск (отладка)" + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "{\n \"Username\": \"gitea\",\n \"Personal Access Token\": \"0f276ee359b823464680037cb9b62f3558796f95\",\n \"Repository URL\": \"http://192.168.1.155:3001\",\n \"Repository Name\": \"workflows\",\n \"Organization\": \"event_forge\",\n \"Branch\": \"main\",\n \"Custom Branch Name\": \"\",\n \"Export Folder\": \"/data/shared\",\n \"Активировать workflow\": false\n}\n", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 5456, + 1136 + ], + "id": "1685976a-f037-4f4e-ad5b-6fed8c808fb8", + "name": "Тестовые данные" + }, + { + "parameters": { + "jsCode": "const body = $input.first().json;\n\nconst username = (body['Username'] || '').trim();\nconst pat = (body['Personal Access Token'] || '').trim();\nconst repoUrl = (body['Repository URL'] || '').trim().replace(/\\/$/, '');\nconst repoName = (body['Repository Name'] || '').trim();\nconst org = (body['Organization'] || '').trim();\nconst exportFolder = (body['Export Folder'] || '/data/shared').trim().replace(/\\/$/, '');\n\n// Определяем ветку\nconst branchType = body['Branch'];\nconst customBranch = (body['Custom Branch Name'] || '').trim();\nlet branch;\nif (branchType === 'custom') {\n if (!customBranch) throw new Error('Custom Branch Name обязателен при выборе custom');\n branch = customBranch;\n} else {\n branch = branchType;\n}\n\nconst authUrl = repoUrl.replace(/^(https?:\\/\\/)/, `$1${encodeURIComponent(username)}:${encodeURIComponent(pat)}@`);\nconst remoteUrl = `${authUrl}/${org}/${repoName}.git`;\nconst cleanRemoteUrl = `${repoUrl}/${org}/${repoName}.git`;\nconst localFolder = `${exportFolder}/${org}/${repoName}`;\nconst activate = !!(body['Активировать workflow']);\n\nreturn [{\n json: {\n username,\n pat,\n repoUrl,\n repoName,\n org,\n branch,\n remoteUrl,\n cleanRemoteUrl,\n localFolder,\n activate,\n exportFolder,\n }\n}];" + }, + "id": "74a303da-5491-4623-a884-298621a18122", + "name": "Подготовка переменных", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 5856, + 912 + ] + }, + { + "parameters": { + "command": "=LOCAL_FOLDER=\"{{ $json.localFolder }}\"\nREMOTE_URL=\"{{ $json.remoteUrl }}\"\nCLEAN_URL=\"{{ $json.cleanRemoteUrl }}\"\nBRANCH=\"{{ $json.branch }}\"\n\nif [ ! -d \"$LOCAL_FOLDER/.git\" ]; then\n # Папки нет или нет .git — клонируем заново\n rm -rf \"$LOCAL_FOLDER\"\n mkdir -p \"$(dirname $LOCAL_FOLDER)\"\n git clone --branch \"$BRANCH\" \"$REMOTE_URL\" \"$LOCAL_FOLDER\" 2>&1\n cd \"$LOCAL_FOLDER\"\nelse\n # Папка есть — обновляем remote и подтягиваем\n cd \"$LOCAL_FOLDER\"\n CURRENT_REMOTE=$(git remote get-url origin 2>/dev/null || echo \"\")\n if [ \"$CURRENT_REMOTE\" != \"$REMOTE_URL\" ]; then\n git remote set-url origin \"$REMOTE_URL\"\n fi\n git fetch origin 2>&1\n git checkout \"$BRANCH\" 2>&1 || git checkout -b \"$BRANCH\" \"origin/$BRANCH\" 2>&1\n git pull origin \"$BRANCH\" 2>&1\nfi\n\n# Очищаем remote от кредов сразу после операций\ngit remote set-url origin \"$CLEAN_URL\"\n\necho \"PULL_SUCCESS: $BRANCH | $LOCAL_FOLDER\"" + }, + "id": "5f20be07-92b1-4ba5-91b6-a95b0947e233", + "name": "Git Clone / Pull", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 6032, + 912 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "pull-success-check", + "leftValue": "={{ $json.exitCode }}", + "rightValue": 0, + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 6208, + 912 + ], + "id": "010d52da-670b-40a0-9177-fb87cb9fa4bf", + "name": "Pull успешен?" + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Ошибка git", + "completionMessage": "=Не удалось получить данные из репозитория:\n\n{{ $json.stderr }}", + "limitWaitTime": true, + "resumeUnit": "minutes", + "options": { + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "type": "n8n-nodes-base.form", + "typeVersion": 2.5, + "position": [ + 6208, + 1136 + ], + "id": "d458c7f0-f038-4bc1-8259-5581f6fe894b", + "name": "Форма: Ошибка git", + "webhookId": "b22c33d4-e55f-6677-b88c-99d00e11f222" + }, + { + "parameters": { + "jsCode": "const fs = require('fs');\nconst path = require('path');\nconst localFolder = $('Подготовка переменных').first().json.localFolder;\n\nlet files;\ntry {\n files = fs.readdirSync(localFolder).filter(f => f.endsWith('.json'));\n} catch (e) {\n throw new Error(`Не удалось прочитать папку ${localFolder}: ${e.message}`);\n}\n\nif (files.length === 0) {\n throw new Error(`В папке ${localFolder} не найдено JSON файлов`);\n}\n\nconst allWorkflows = $input.all().map(item => item.json);\n\nconst results = [];\n\nfor (const fileName of files) {\n const filePath = path.join(localFolder, fileName);\n try {\n const raw = fs.readFileSync(filePath, 'utf8');\n const parsed = JSON.parse(raw);\n const wfData = Array.isArray(parsed) ? parsed[0] : parsed;\n\n if (!wfData || !wfData.name) {\n console.log(`SKIP: ${fileName} — нет поля name`);\n continue;\n }\n\n const existing = allWorkflows.find(wf => wf.name === wfData.name);\n\n let fileStatus;\n if (!existing) {\n fileStatus = 'NEW';\n } else {\n fileStatus = existing.updatedAt === wfData.updatedAt ? 'UNCHANGED' : 'CHANGED';\n }\n\n // Признак активации берём из файла\n const wfActive = !!(wfData.active);\n\n results.push({\n json: {\n fileName,\n filePath,\n fileStatus,\n workflowName: wfData.name,\n workflowData: wfData,\n existingId: existing ? existing.id : null,\n wfActive,\n }\n });\n\n console.log(`${fileStatus} [active=${wfActive}]: ${fileName}`);\n } catch (e) {\n console.log(`SKIP: ${fileName} — ошибка парсинга: ${e.message}`);\n }\n}\n\nif (results.length === 0) {\n throw new Error('Ни один файл не удалось распарсить');\n}\n\nconsole.log(`Прочитано файлов: ${results.length} из ${files.length}`);\nreturn results;" + }, + "id": "4e566ce2-e54b-4c56-92ed-ff0eefbad2bc", + "name": "Прочитать файлы из папки", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 6800, + 912 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "d56efecf-5f77-4cd8-a149-4f3fefdd0faa", + "name": "Цикл по файлам", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 7152, + 912 + ] + }, + { + "parameters": { + "jsCode": "const results = $input.all().map(item => item.json);\n\nconst total = results.length;\nconst created = results.filter(r => r.fileStatus === 'NEW' && r.imported).length;\nconst updated = results.filter(r => r.fileStatus === 'CHANGED' && r.imported).length;\nconst skipped = results.filter(r => r.fileStatus === 'UNCHANGED').length;\nconst failed = results.filter(r => r.fileStatus !== 'UNCHANGED' && !r.imported).length;\nconst activated = results.filter(r => r.activated).length;\n\nconst details = results.map(r => {\n if (r.fileStatus === 'UNCHANGED') return `➖ Пропущен: ${r.workflowName}`;\n const act = r.activated ? ' ▶️' : '';\n if (r.imported && r.fileStatus === 'NEW') return `✅ Создан: ${r.workflowName}${act}`;\n if (r.imported && r.fileStatus === 'CHANGED') return `🔄 Обновлён: ${r.workflowName}${act}`;\n return `❌ Ошибка: ${r.workflowName}`;\n}).join('\\n');\n\nconst activatedLine = activated > 0 ? ` | ▶️ Активировано: ${activated}` : '';\n\nreturn [{\n json: {\n total,\n created,\n updated,\n skipped,\n failed,\n activated,\n summary: `Всего: ${total} | Создано: ${created} | Обновлено: ${updated} | Пропущено: ${skipped} | Ошибок: ${failed}${activatedLine}`,\n details,\n }\n}];" + }, + "id": "2ef6e478-b1b5-401f-8d37-d0c252e4c651", + "name": "Сформировать сводку", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 8368, + 912 + ] + }, + { + "parameters": { + "operation": "completion", + "completionTitle": "Импорт завершён", + "completionMessage": "={{ $json.summary }}\n\n{{ $json.details }}\n\n⚠️ Не забудьте перепривязать credentials в импортированных workflow.", + "limitWaitTime": true, + "resumeUnit": "minutes", + "options": { + "customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}" + } + }, + "type": "n8n-nodes-base.form", + "typeVersion": 2.5, + "position": [ + 8544, + 912 + ], + "id": "e615b2d1-d874-4288-9fac-ad4ba252674e", + "name": "Форма: Импорт завершён", + "webhookId": "c33d44e5-f66a-7788-c99d-00e11f2233g3" + }, + { + "parameters": { + "command": "=n8n import:workflow --input=\"{{ $json.filePath }}\"\necho \"IMPORTED: {{ $json.workflowName }}\"" + }, + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 7760, + 976 + ], + "id": "87616442-e9c5-47fd-bc3a-0993b7f02408", + "name": "Импортировать workflow" + }, + { + "parameters": { + "jsCode": "const item = $input.first().json;\nconst stdout = item.stdout || '';\n\n// Парсим строки лога — каждая строка это JSON\nconst lines = stdout.split('\\n').filter(Boolean);\nconst logMessages = [];\n\nfor (const line of lines) {\n try {\n const parsed = JSON.parse(line);\n logMessages.push(parsed.message);\n } catch (e) {\n // не JSON строка — обычный текст (IMPORTED: ...)\n }\n}\n\nconst success = item.exitCode === 0;\nconst workflowName = stdout.match(/IMPORTED: (.+)/)?.[1]?.trim() || 'неизвестно';\n\nreturn [{\n json: {\n workflowName,\n success,\n message: logMessages.join(' → '),\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7184, + 1200 + ], + "id": "35a93ed9-385d-4372-9c97-3b0cf0e56598", + "name": "Разбор результата импорта" + }, + { + "parameters": { + "filters": {}, + "requestOptions": {} + }, + "type": "n8n-nodes-base.n8n", + "typeVersion": 1, + "position": [ + 6576, + 912 + ], + "id": "3695a508-3511-485d-b641-b8e8202c39fc", + "name": "Получить все workflow n8n", + "credentials": { + "n8nApi": { + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local" + } + } + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "leftValue": "={{ $json.fileStatus }}", + "rightValue": "NEW", + "operator": { + "type": "string", + "operation": "equals" + }, + "id": "e34cf093-cfc1-4ad6-98bd-6fa71911bd8c" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "NEW" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "5fc2aed6-0795-4d31-a763-13269a9d889c", + "leftValue": "={{ $json.fileStatus }}", + "rightValue": "CHANGED", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "CHANGED" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "8b126f2d-d9bc-479a-b1ad-bdc7d429ca32", + "leftValue": "={{ $json.fileStatus }}", + "rightValue": "UNCHANGED", + "operator": { + "type": "string", + "operation": "equals", + "name": "filter.operator.equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "UNCHANGED" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.switch", + "typeVersion": 3.4, + "position": [ + 7360, + 976 + ], + "id": "3d8141e0-fdb7-464e-8f9e-87cef96b0f37", + "name": "Маршрутизация по статусу" + }, + { + "parameters": { + "jsCode": "const fromSwitch = $('Маршрутизация по статусу').item.json;\nconst fromImport = $('Разбор результата импорта').item.json;\n\n// Активация через REST API: успех = нода вернула объект с active=true\nlet activated = false;\nlet activatedId = null;\ntry {\n const out = $('Активировать workflow').item.json;\n activated = out && out.active === true;\n activatedId = out && out.id ? out.id : null;\n} catch (e) {\n // Ветка FALSE у IF — ноду активации не проходили\n}\n\nreturn [{\n json: {\n fileName: fromSwitch.fileName,\n workflowName: fromSwitch.workflowName,\n fileStatus: fromSwitch.fileStatus,\n existingId: fromSwitch.existingId,\n activatedId,\n imported: fromImport.success,\n activated,\n message: fromImport.message,\n importedAt: new Date().toISOString(),\n }\n}];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7968, + 1200 + ], + "id": "08d1f524-a66f-4804-9d40-6dd98da463f8", + "name": "Записать результат" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "activate-global", + "leftValue": "={{ $('Подготовка переменных').first().json.activate }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + }, + { + "id": "activate-wf-active", + "leftValue": "={{ $('Маршрутизация по статусу').item.json.wfActive }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + }, + { + "id": "activate-success", + "leftValue": "={{ $json.success }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 7360, + 1200 + ], + "id": "c98aa5ee-1df4-441d-9648-539f841a3794", + "name": "Нужно активировать?" + }, + { + "parameters": { + "operation": "activate", + "workflowId": "={{ $json.id }}", + "additionalFields": {}, + "requestOptions": {} + }, + "type": "n8n-nodes-base.n8n", + "typeVersion": 1, + "position": [ + 7776, + 1200 + ], + "id": "8c91c94d-c6af-4249-a226-d280cb7692c4", + "name": "Активировать workflow", + "credentials": { + "n8nApi": { + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local" + } + }, + "onError": "continueRegularOutput" + }, + { + "parameters": { + "jsCode": "const fromSwitch = $('Маршрутизация по статусу').item.json;\n\nreturn [{\n json: {\n fileName: fromSwitch.fileName,\n workflowName: fromSwitch.workflowName,\n fileStatus: 'UNCHANGED',\n existingId: fromSwitch.existingId,\n imported: false,\n activated: false,\n message: 'Пропущен — без изменений',\n importedAt: new Date().toISOString(),\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 7968, + 1024 + ], + "id": "1410b0c4-1ee9-4b22-b48a-017c8f362549", + "name": "Записать результат (пропущен)" + }, + { + "parameters": { + "filters": { + "name": "={{ $('Маршрутизация по статусу').item.json.workflowName }}" + }, + "requestOptions": {} + }, + "type": "n8n-nodes-base.n8n", + "typeVersion": 1, + "position": [ + 7568, + 1200 + ], + "id": "4baec163-2102-4114-982b-eba38a38beca", + "name": "Найти id после импорта", + "credentials": { + "n8nApi": { + "id": "aLPzwPxLHLLpPJIw", + "name": "n8n local" + } + } + } + ], + "pinData": {}, + "connections": { + "Форма запроса": { + "main": [ + [ + { + "node": "Подготовка переменных", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ручной запуск (отладка)": { + "main": [ + [ + { + "node": "Тестовые данные", + "type": "main", + "index": 0 + } + ] + ] + }, + "Тестовые данные": { + "main": [ + [ + { + "node": "Подготовка переменных", + "type": "main", + "index": 0 + } + ] + ] + }, + "Подготовка переменных": { + "main": [ + [ + { + "node": "Git Clone / Pull", + "type": "main", + "index": 0 + } + ] + ] + }, + "Git Clone / Pull": { + "main": [ + [ + { + "node": "Pull успешен?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pull успешен?": { + "main": [ + [ + { + "node": "Получить все workflow n8n", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Форма: Ошибка git", + "type": "main", + "index": 0 + } + ] + ] + }, + "Прочитать файлы из папки": { + "main": [ + [ + { + "node": "Цикл по файлам", + "type": "main", + "index": 0 + } + ] + ] + }, + "Цикл по файлам": { + "main": [ + [ + { + "node": "Сформировать сводку", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Маршрутизация по статусу", + "type": "main", + "index": 0 + } + ] + ] + }, + "Сформировать сводку": { + "main": [ + [ + { + "node": "Форма: Импорт завершён", + "type": "main", + "index": 0 + } + ] + ] + }, + "Импортировать workflow": { + "main": [ + [ + { + "node": "Разбор результата импорта", + "type": "main", + "index": 0 + } + ] + ] + }, + "Разбор результата импорта": { + "main": [ + [ + { + "node": "Нужно активировать?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Получить все workflow n8n": { + "main": [ + [ + { + "node": "Прочитать файлы из папки", + "type": "main", + "index": 0 + } + ] + ] + }, + "Маршрутизация по статусу": { + "main": [ + [ + { + "node": "Импортировать workflow", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Импортировать workflow", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Записать результат (пропущен)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Записать результат": { + "main": [ + [ + { + "node": "Цикл по файлам", + "type": "main", + "index": 0 + } + ] + ] + }, + "Нужно активировать?": { + "main": [ + [ + { + "node": "Найти id после импорта", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Записать результат", + "type": "main", + "index": 0 + } + ] + ] + }, + "Активировать workflow": { + "main": [ + [ + { + "node": "Записать результат", + "type": "main", + "index": 0 + } + ] + ] + }, + "Записать результат (пропущен)": { + "main": [ + [ + { + "node": "Цикл по файлам", + "type": "main", + "index": 0 + } + ] + ] + }, + "Найти id после импорта": { + "main": [ + [ + { + "node": "Активировать workflow", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "cef6215b-db51-43c0-ac6c-65570a2c30dc", + "meta": { + "instanceId": "f720a996c27dff6663621d1fe2231dde9f00009fb064ea1db630bfc1dc798ddd" + }, + "id": "0VqMxJC45kM28mgK", + "tags": [] +} \ No newline at end of file diff --git a/n8n/runner/Dockerfile.runner b/n8n/runner/Dockerfile.runner new file mode 100644 index 0000000..6759506 --- /dev/null +++ b/n8n/runner/Dockerfile.runner @@ -0,0 +1,26 @@ +FROM n8nio/runners:stable + +USER root +COPY requirements.txt /tmp/requirements.txt + +# Установка Python пакетов через uv из requirements.txt +RUN if [ -f /tmp/requirements.txt ]; then \ + echo "Installing Python packages from requirements.txt using uv..." && \ + echo "Contents:" && \ + cat /tmp/requirements.txt && \ + cd /opt/runners/task-runner-python && \ + uv pip install -r /tmp/requirements.txt && \ + echo "Python packages installed successfully"; \ + else \ + echo "No requirements.txt found, skipping Python packages installation"; \ + fi + +# Установка JavaScript пакетов +RUN cd /opt/runners/task-runner-javascript && \ + echo "Installing JS packages..." && \ + pnpm add moment uuid && \ + echo "JS packages installed successfully" + +COPY n8n-task-runners.json /etc/n8n-task-runners.json + +USER runner diff --git a/n8n/runner/n8n-task-runners.json b/n8n/runner/n8n-task-runners.json new file mode 100644 index 0000000..a70f7dc --- /dev/null +++ b/n8n/runner/n8n-task-runners.json @@ -0,0 +1,56 @@ +{ + "task-runners": [ + { + "runner-type": "javascript", + "workdir": "/home/runner", + "command": "/usr/local/bin/node", + "args": [ + "--disallow-code-generation-from-strings", + "--disable-proto=delete", + "/opt/runners/task-runner-javascript/dist/start.js" + ], + "health-check-server-port": "5681", + "allowed-env": [ + "PATH", + "GENERIC_TIMEZONE", + "NODE_OPTIONS", + "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT", + "N8N_RUNNERS_TASK_TIMEOUT", + "N8N_RUNNERS_MAX_CONCURRENCY", + "N8N_SENTRY_DSN", + "ENVIRONMENT", + "DEPLOYMENT_NAME", + "HOME" + ], + "env-overrides": { + "NODE_FUNCTION_ALLOW_BUILTIN": "*", + "NODE_FUNCTION_ALLOW_EXTERNAL": "*", + "NODE_PATH": "/home/runner/node_modules", + "N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST": "0.0.0.0" + } + }, + { + "runner-type": "python", + "workdir": "/home/runner", + "command": "/opt/runners/task-runner-python/.venv/bin/python", + "args": ["-m", "src.main"], + "health-check-server-port": "5682", + "allowed-env": [ + "PATH", + "GENERIC_TIMEZONE", + "N8N_RUNNERS_LAUNCHER_LOG_LEVEL", + "N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT", + "N8N_RUNNERS_TASK_TIMEOUT", + "N8N_RUNNERS_MAX_CONCURRENCY", + "N8N_SENTRY_DSN", + "ENVIRONMENT", + "DEPLOYMENT_NAME" + ], + "env-overrides": { + "PYTHONPATH": "/opt/runners/task-runner-python", + "N8N_RUNNERS_STDLIB_ALLOW": "*", + "N8N_RUNNERS_EXTERNAL_ALLOW": "*" + } + } + ] +} diff --git a/n8n/runner/requirements.txt b/n8n/runner/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/n8n/runner/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..f43cb66 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,7 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: "n8n" + static_configs: + - targets: ["n8n:5678"] diff --git a/scripts/import_workflows.sh b/scripts/import_workflows.sh new file mode 100644 index 0000000..92bdfad --- /dev/null +++ b/scripts/import_workflows.sh @@ -0,0 +1,112 @@ +#!/bin/sh +# Import n8n workflows and credentials from backup directory +# This script runs inside the n8n-import container +# +# Credentials skip logic: +# После успешного импорта создаётся маркер в /home/node/.n8n/imported/ +# При повторном запуске файл с существующим маркером пропускается. +# Это позволяет не перезатирать API-ключи при повторном деплое. +# Чтобы принудительно переимпортировать — удалить маркер или задать FORCE_IMPORT=true + +set -e + +MARKER_DIR="/home/node/.n8n/imported" +mkdir -p "$MARKER_DIR" + +COUNTER_FILE=$(mktemp) +trap 'rm -f "$COUNTER_FILE"' EXIT +echo "0" > "$COUNTER_FILE" + +# Import credentials first +echo 'Importing credentials...' +CRED_FILES=$(find /backup/credentials -maxdepth 1 -type f -not -name '.gitkeep' 2>/dev/null || true) +if [ -n "$CRED_FILES" ]; then + CRED_COUNT=$(echo "$CRED_FILES" | wc -l | tr -d ' ') + + # Normalize credentials files (wrap object in array if needed) + echo "$CRED_FILES" | while IFS= read -r file; do + FIRST_CHAR=$(head -c 1 "$file") + if [ "$FIRST_CHAR" = "{" ]; then + TMPFILE=$(mktemp) + echo "[$(cat "$file")]" > "$TMPFILE" + mv "$TMPFILE" "$file" + fi + done + + echo "0" > "$COUNTER_FILE" + echo "$CRED_FILES" | while IFS= read -r file; do + CURRENT=$(cat "$COUNTER_FILE") + CURRENT=$((CURRENT + 1)) + echo "$CURRENT" > "$COUNTER_FILE" + filename=$(basename "$file") + MARKER="$MARKER_DIR/cred_$(echo "$filename" | tr '/' '_')" + + printf "[%2d/%d] %s" "$CURRENT" "$CRED_COUNT" "$filename" + + # Пропустить если уже импортировался и FORCE_IMPORT не задан + if [ -f "$MARKER" ] && [ "$FORCE_IMPORT" != "true" ]; then + echo " SKIPPED (already imported, set FORCE_IMPORT=true to override)" + continue + fi + + OUTPUT_FILE=$(mktemp) + + if n8n import:credentials --input="$file" > "$OUTPUT_FILE" 2>&1; then + echo " OK" + touch "$MARKER" + if [ -s "$OUTPUT_FILE" ]; then + echo " Details: $(cat "$OUTPUT_FILE" | tr '\n' ' ')" + fi + else + echo " FAILED" + echo " Error: $(cat "$OUTPUT_FILE" | tr '\n' ' ')" + fi + + rm -f "$OUTPUT_FILE" + done +fi + +# Import workflows +echo '' +echo 'Importing workflows...' +WORKFLOW_FILES=$(find /backup/workflows -maxdepth 1 -type f -not -name '.gitkeep' 2>/dev/null || true) +if [ -z "$WORKFLOW_FILES" ]; then + echo 'No workflows found to import.' + exit 0 +fi +TOTAL_FOUND=$(echo "$WORKFLOW_FILES" | wc -l | tr -d ' ') +if [ -n "$IMPORT_LIMIT" ]; then + WORKFLOW_FILES=$(echo "$WORKFLOW_FILES" | head -n "$IMPORT_LIMIT") +fi +TOTAL=$(echo "$WORKFLOW_FILES" | wc -l | tr -d ' ') +echo "Importing $TOTAL of $TOTAL_FOUND workflows" +echo '' +echo "0" > "$COUNTER_FILE" +echo "$WORKFLOW_FILES" | while IFS= read -r file; do + CURRENT=$(cat "$COUNTER_FILE") + CURRENT=$((CURRENT + 1)) + echo "$CURRENT" > "$COUNTER_FILE" + filename=$(basename "$file") + printf "[%3d/%d] %s" "$CURRENT" "$TOTAL" "$filename" + + OUTPUT_FILE=$(mktemp) + + if n8n import:workflow --input="$file" > "$OUTPUT_FILE" 2>&1; then + echo " OK" + if [ -s "$OUTPUT_FILE" ]; then + OUTPUT=$(cat "$OUTPUT_FILE") + if echo "$OUTPUT" | grep -q "id"; then + echo " Imported: $(echo "$OUTPUT" | tr '\n' ' ' | sed 's/[{}"]//g')" + else + echo " Details: $OUTPUT" + fi + fi + else + echo " FAILED" + echo " Error: $(cat "$OUTPUT_FILE" | tr '\n' ' ')" + fi + + rm -f "$OUTPUT_FILE" +done +echo '' +echo 'Import complete!'