Files
n8n_stack/n8n/backup/workflows/Git_Commit_Workflows.json
T
V.Bolshakov 49b389ed53 first commit
2026-05-04 16:54:53 +07:00

1105 lines
50 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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=<workflowId> --output=<filePath>`\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://<URL Repo>",
"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": []
}