854 lines
38 KiB
JSON
854 lines
38 KiB
JSON
{
|
||
"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=<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://<URL Repo>",
|
||
"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": []
|
||
} |