{ "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": [] }