From 531b6039c62a725c3d3f5c086f216ed102bdd002 Mon Sep 17 00:00:00 2001 From: Striker Date: Mon, 23 Mar 2026 04:05:11 +0300 Subject: [PATCH] =?UTF-8?q?Add=20Plane=E2=86=92Singularity=20workflow=20do?= =?UTF-8?q?cs=20and=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: описание репозитория, список workflows - workflows/plane-singularity/README.md: полная документация по интеграции (схема, payload структура, маппинг 28 проектов, Singularity API) - workflows/plane-singularity/update_workflow.py: скрипт обновления через n8n API Co-Authored-By: Claude Sonnet 4.6 --- README.md | 17 +- workflows/plane-singularity/README.md | 135 ++++++++++ .../plane-singularity/update_workflow.py | 234 ++++++++++++++++++ 3 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 workflows/plane-singularity/README.md create mode 100644 workflows/plane-singularity/update_workflow.py diff --git a/README.md b/README.md index d8405b5..d41233d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# n8n-workflows +# n8n Workflows -n8n workflow configs and documentation \ No newline at end of file +n8n workflows и документация. Инстанс: https://n8n.striker.su + +## Workflows + +| Workflow | ID | Описание | +|----------|-----|----------| +| [Plane → Singularity](workflows/plane-singularity/README.md) | `zyO77cIUNdbc4xPH` | Создание/закрытие задач Singularity по событиям Plane | + +## Инфраструктура + +- **URL:** https://n8n.striker.su +- **Сервер:** str-u-01.striker.su (45.10.53.148) +- **Docker Compose:** `/opt/n8n/docker-compose.yml` +- **Data:** `/opt/n8n/data` diff --git a/workflows/plane-singularity/README.md b/workflows/plane-singularity/README.md new file mode 100644 index 0000000..78b27cd --- /dev/null +++ b/workflows/plane-singularity/README.md @@ -0,0 +1,135 @@ +# Plane → Singularity: назначение задачи + +**Workflow ID:** `zyO77cIUNdbc4xPH` +**n8n URL:** https://n8n.striker.su +**Создан:** 2026-03-23 + +## Назначение + +Автоматически синхронизирует задачи из Plane в Singularity: + +- **Назначена на меня** → создаёт задачу в Singularity +- **Задача завершена** → находит задачу в Singularity и закрывает её + +## Схема + +``` +Webhook (POST /webhook/plane-assignment) + | +Switch "Тип события" + |-- [assignee_ids содержит gen.director@hhivp.com] + | | + | Code "Подготовить данные" (маппинг проекта, приоритета, дедлайна) + | | + | HTTP POST /v2/task (создать задачу в Singularity) + | + +-- [state_id -> state.group == completed] + | + HTTP GET /v2/task?search=PROJ-42 (найти задачу) + | + HTTP PATCH /v2/task/{id} (закрыть {"complete": 1}) +``` + +## Plane Webhook + +Зарегистрирован через PostgreSQL (API endpoint /api/v1/workspaces/.../webhooks/ недоступен): + +```sql +-- На сервере Plane (rd.hhivp.com), БД: plane +INSERT INTO webhooks (id, url, is_active, workspace, issue, created_at, updated_at) +VALUES ( + '1d053279-658a-4987-8ee0-303f2c7b952e', + 'https://n8n.striker.su/webhook/plane-assignment', + true, + 'c9e0fe9f-02dd-4bd4-9026-d8783e10c4b2', + true, + NOW(), NOW() +); +``` + +## Реальная структура payload от Plane + +```json +{ + "event": "issue", + "action": "updated", + "data": { + "project": "", + "project_identifier": "TCKT", + "sequence_id": 42, + "name": "Название задачи", + "priority": "high", + "target_date": "2026-04-01", + "description_stripped": "...", + "state": { "group": "completed" } + }, + "activity": { + "field": "assignee_ids", + "new_value": ["f7a5e9db-eaf0-4314-9440-4b28094f5db1"], + "old_value": null + } +} +``` + +> ВАЖНО: Plane шлёт field="assignee_ids" (не "assignees") и field="state_id" (не "state"). +> new_value для assignee -- массив строк UUID, не строка. + +## Пользователь + +| Параметр | Значение | +|----------|----------| +| Email | gen.director@hhivp.com | +| UUID | f7a5e9db-eaf0-4314-9440-4b28094f5db1 | + +## Маппинг проектов Plane -> Singularity + +| Plane UUID (сокращ.) | Идентификатор | Singularity Project | +|----------------------|---------------|---------------------| +| 0a821ae1 | DRC | P-b40d30ed (ДРЦ Нагорное) | +| 87598e10 | NAVISCOPE | P-00ddf477 | +| 1099af67 | NAB | P-30340426 (Прочее) | +| 2d35134d | MANYH | P-5a0bd9ad (ИП Маняхин) | +| 1f4bcaee | TCKT | P-638ec16a (HHIVP Инфра) | +| 74d5b2f9 | CIFRA | P-63c01156 (ЦифраЦифра) | +| 26cd4aab | VEHA | P-39d04293 (Веха) | +| 66748cf3 | SINVS | P-b1a9d408 (Олимпийский 42а) | +| 309e735f | SHAUS | P-8624fef6 (СтифтерХаус) | +| 404b4c7a | YAR | P-ea61bc07 (Ярослав В.) | +| 89133e7f | SBOR | P-e4c50922 (Старый Большевик) | +| dd3495bf | SMED | P-cec926a1 (Северное Медведково) | +| 94e0ddd0 | SAMOI | P-e73fe9a8 (А. Самойлов) | +| a3906157 | HG | P-32c6abd8 (Сад Здоровья) | +| ec70a115 | ROMA | P-281f799e (Ромашка) | +| 04427cbb | NIIH | P-30340426 (Прочее) | +| 191c0a5b | LIANZ | P-7377d35f (Лианозово) | +| 5db759d0 | MOIS | P-db73597a (ИП Моисеев) | +| 60c0bd07 | VONDI | P-04b65df5 (Вондига) | +| c4cdd5fb | 4101 | P-30340426 (Прочее) | +| c6b2531b | BCOM | P-30340426 (Прочее) | +| 91a97d40 | DELTA | P-62de7005 (Дельта) | +| eaef875f | WEB | P-638ec16a (HHIVP Инфра) | +| 51846118 | REGRU | P-a3f177b1 (Домены) | +| 910aa865 | HARZL | P-05ca29bb (ХарцЛабс) | +| dfd2a49f | 3DRU | P-181d5832 (3Д.РУ) | +| 46f45846 | IVA | P-d7a92af2 (Ива) | +| 4df07960 | HHVIP | P-638ec16a (HHIVP Инфра) | + +Неизвестный проект -> P-91f69023 (fallback) + +## Singularity API + +- Base URL: https://api.singularity-app.com/v2 +- Auth: Bearer 11a4eac1-3dd9-4448-99f6-0b3c58315a5d +- Create task: POST /task +- Search task: GET /task?search=PROJ-42 +- Close task: PATCH /task/{id} body: {"complete": 1} + +## Обновление workflow + +Скрипт для обновления через n8n API: `update_workflow.py` + +```bash +python update_workflow.py +``` + +n8n API: PUT https://n8n.striker.su/api/v1/workflows/zyO77cIUNdbc4xPH diff --git a/workflows/plane-singularity/update_workflow.py b/workflows/plane-singularity/update_workflow.py new file mode 100644 index 0000000..a495a41 --- /dev/null +++ b/workflows/plane-singularity/update_workflow.py @@ -0,0 +1,234 @@ +import json, urllib.request + +N8N_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOTBmYzEwZS1jY2JmLTQwZWQtOWIzYy0wNmIwMGY2N2QwMjEiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwianRpIjoiNGM5M2IwNjMtNWQ3My00MTU0LWIyMjYtYWM0NDFmZmNmYTRiIiwiaWF0IjoxNzc0MjE3MDM2fQ.7uga-4ukI2uJSmRRep2ijlhy6x0I92nFVQl2nxMX1VU" + +workflow = { + "name": "Plane → Singularity: назначение задачи", + "nodes": [ + { + "id": "1", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [240, 300], + "parameters": { + "httpMethod": "POST", + "path": "plane-assignment", + "responseMode": "onReceived", + "responseData": "firstEntryJson" + }, + "webhookId": "plane-assignment" + }, + { + "id": "2", + "name": "Тип события", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [480, 300], + "parameters": { + "mode": "rules", + "rules": { + "values": [ + { + "conditions": { + "options": {"caseSensitive": True, "leftValue": "", "typeValidation": "strict"}, + "conditions": [ + { + "id": "a1", + "leftValue": "={{ $json.body.activity.field }}", + "rightValue": "assignee_ids", + "operator": {"type": "string", "operation": "equals"} + }, + { + "id": "a2", + "leftValue": "={{ JSON.stringify($json.body.activity.new_value) }}", + "rightValue": "f7a5e9db-eaf0-4314-9440-4b28094f5db1", + "operator": {"type": "string", "operation": "contains"} + } + ], + "combinator": "and" + }, + "renameOutput": True, + "outputKey": "Назначена на меня" + }, + { + "conditions": { + "options": {"caseSensitive": True, "leftValue": "", "typeValidation": "strict"}, + "conditions": [ + { + "id": "b1", + "leftValue": "={{ $json.body.activity.field }}", + "rightValue": "state_id", + "operator": {"type": "string", "operation": "equals"} + }, + { + "id": "b2", + "leftValue": "={{ $json.body.data.state.group }}", + "rightValue": "completed", + "operator": {"type": "string", "operation": "equals"} + } + ], + "combinator": "and" + }, + "renameOutput": True, + "outputKey": "Задача завершена" + } + ] + }, + "options": {} + } + }, + { + "id": "6", + "name": "Подготовить данные", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [720, 160], + "parameters": { + "jsCode": """const data = $json.body.data; +const activity = $json.body.activity; + +const priorityMap = { + urgent: 0, + high: 0, + medium: 1, + low: 2, + none: 2 +}; + +const projectMap = { + '0a821ae1-7564-431d-a78c-c76885d85a9a': 'P-b40d30ed-8480-47a0-b0fb-04005eab2028', // DRC - ДРЦ(Нагорное) + '87598e10-0c7d-4612-8a38-073085358580': 'P-00ddf477-f600-44fb-a177-db2af3861dfe', // NAVISCOPE + '1099af67-70b7-4260-90fd-31de5e82e05a': 'P-30340426-ff0e-40b7-af17-070372936101', // NAB - Прочее + '2d35134d-6055-4a88-be9e-a0ee49f018e9': 'P-5a0bd9ad-3df2-4d9c-a7bf-3f59621a2309', // MANYH - ИП Маняхин + '1f4bcaee-1ff7-4227-b160-8aa6561c55e7': 'P-638ec16a-9544-4161-ab39-014ca44772c2', // TCKT - HHIVP Инфра + '74d5b2f9-558c-4371-ab6a-21a082b33933': 'P-63c01156-e0fe-491d-a4f0-82fe4e0c4af3', // CIFRA - ЦифраЦифра + '26cd4aab-cad6-46ae-924b-57d911128506': 'P-39d04293-a3a3-4110-b246-db67dd4729cc', // VEHA - Веха + '66748cf3-1e67-4c7d-9c17-44e90854e7e3': 'P-b1a9d408-635e-4cf2-9b40-b830749fb996', // SINVS - Олимпийский 42а + '309e735f-5fd2-4090-a8ba-df5948532e21': 'P-8624fef6-01ec-44bf-b578-816d90ef56ec', // SHAUS - СтифтерХаус + '404b4c7a-cce0-4126-aa0e-3fd61ff53a14': 'P-ea61bc07-5c60-4b91-99e2-66d4c545338a', // YAR - Ярослав В. + '89133e7f-45d7-44ed-85b9-87f12634b0d4': 'P-e4c50922-162a-445f-a8e1-5621d2ef4124', // SBOR - Старый Большевик + 'dd3495bf-3d49-4e30-b614-35e2362fad4f': 'P-cec926a1-8425-42cc-99e1-2079b7b1298f', // SMED - Северное Медведково + '94e0ddd0-200f-4eb7-8f89-a86ec8e63df4': 'P-e73fe9a8-ada5-46d4-9e8a-2ce5b889d95e', // SAMOI - А. Самойлов + 'a3906157-53e4-47d0-b3c8-89b807d36ef7': 'P-32c6abd8-2246-4240-b1f0-c85c2a221331', // HG - Сад Здоровья + 'ec70a115-6d14-43c9-8572-bd423bcf7289': 'P-281f799e-304b-4a19-bd9d-db0411e4335a', // ROMA - Ромашка + '04427cbb-d108-4558-8aa1-ad7a486e1f9b': 'P-30340426-ff0e-40b7-af17-070372936101', // NIIH - Прочее + '191c0a5b-e998-4503-84c7-aaee67093393': 'P-7377d35f-eb9e-4aa4-828a-1bc01b000624', // LIANZ - Лианозово + '5db759d0-a5fb-453a-90ca-30b0e6355635': 'P-db73597a-47b2-44b3-8282-f9f0e29d52df', // MOIS - ИП Моисеев + '60c0bd07-4867-4c8a-b6e3-b7c78701b36d': 'P-04b65df5-b055-4a30-9bb6-4ffbb3146523', // VONDI - Вондига + 'c4cdd5fb-c5c7-4b80-b209-a5d6311090ec': 'P-30340426-ff0e-40b7-af17-070372936101', // 4101 - Прочее + 'c6b2531b-7556-4f66-a646-9d4823ee0a82': 'P-30340426-ff0e-40b7-af17-070372936101', // BCOM - Прочее + '91a97d40-b389-49be-9887-0e791a3bb0f9': 'P-62de7005-33fd-4012-b6ac-7ce277cd0b5d', // DELTA - Дельта + 'eaef875f-0d90-4828-93af-976199913078': 'P-638ec16a-9544-4161-ab39-014ca44772c2', // WEB - HHIVP Инфра + '51846118-c503-47a3-a94a-88aabf1b950a': 'P-a3f177b1-5078-4e05-9213-32d6af16827f', // REGRU - Домены + '910aa865-a8fa-416a-9c5c-c18cb4d2c653': 'P-05ca29bb-d9f8-4668-891f-92dfbe76fee9', // HARZL - ХарцЛабс + 'dfd2a49f-5f5c-48c7-8000-476c56d39f62': 'P-181d5832-3468-4b4a-b1bd-92faa00d580b', // 3DRU - 3Д.РУ + '46f45846-116e-4dba-8ecc-6ba87a5b60f7': 'P-d7a92af2-06bf-4226-a7f4-fc1a2469fe0f', // IVA - Ива + '4df07960-f664-4aba-a757-94a1106c9bae': 'P-638ec16a-9544-4161-ab39-014ca44772c2' // HHVIP - HHIVP Инфра +}; + +const singularityProjectId = projectMap[data.project] || 'P-91f69023-bdd0-43c7-9caa-7994ec2b8cc8'; + +const result = { + title: `[${data.project_identifier}-${data.sequence_id}] ${data.name}`, + note: data.description_stripped || '', + priority: priorityMap[data.priority] ?? 1, + projectId: singularityProjectId, + issueName: data.name, + issueIdentifier: `${data.project_identifier}-${data.sequence_id}` +}; + +if (data.target_date) { + result.deadline = data.target_date; +} + +return [{ json: result }];""" + } + }, + { + "id": "3", + "name": "Создать задачу в Singularity", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [960, 160], + "parameters": { + "method": "POST", + "url": "https://api.singularity-app.com/v2/task", + "sendHeaders": True, + "headerParameters": { + "parameters": [{"name": "Authorization", "value": "Bearer 11a4eac1-3dd9-4448-99f6-0b3c58315a5d"}] + }, + "sendBody": True, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({\n title: $json.title,\n note: $json.note,\n priority: $json.priority,\n projectId: $json.projectId,\n ...($json.deadline ? { deadline: $json.deadline } : {})\n}) }}" + } + }, + { + "id": "4", + "name": "Найти задачу в Singularity", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [720, 440], + "parameters": { + "method": "GET", + "url": "=https://api.singularity-app.com/v2/task?search={{ encodeURIComponent($json.body.data.project_identifier + \"-\" + $json.body.data.sequence_id) }}", + "sendHeaders": True, + "headerParameters": { + "parameters": [{"name": "Authorization", "value": "Bearer 11a4eac1-3dd9-4448-99f6-0b3c58315a5d"}] + } + } + }, + { + "id": "5", + "name": "Закрыть задачу в Singularity", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [960, 440], + "parameters": { + "method": "PATCH", + "url": "=https://api.singularity-app.com/v2/task/{{ $json.tasks[0].id }}", + "sendHeaders": True, + "headerParameters": { + "parameters": [{"name": "Authorization", "value": "Bearer 11a4eac1-3dd9-4448-99f6-0b3c58315a5d"}] + }, + "sendBody": True, + "contentType": "json", + "body": "{\"complete\": 1}" + } + } + ], + "connections": { + "Webhook": {"main": [[{"node": "Тип события", "type": "main", "index": 0}]]}, + "Тип события": { + "main": [ + [{"node": "Подготовить данные", "type": "main", "index": 0}], + [{"node": "Найти задачу в Singularity", "type": "main", "index": 0}] + ] + }, + "Подготовить данные": {"main": [[{"node": "Создать задачу в Singularity", "type": "main", "index": 0}]]}, + "Найти задачу в Singularity": {"main": [[{"node": "Закрыть задачу в Singularity", "type": "main", "index": 0}]]} + }, + "settings": {"executionOrder": "v1"} +} + +body = json.dumps(workflow, ensure_ascii=False).encode('utf-8') +req = urllib.request.Request( + "https://n8n.striker.su/api/v1/workflows/zyO77cIUNdbc4xPH", + data=body, + method="PUT", + headers={ + "X-N8N-API-KEY": N8N_KEY, + "Content-Type": "application/json; charset=utf-8" + } +) +with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + print("OK, id:", result.get("id")) + # Check the switch node conditions + for node in result.get("nodes", []): + if node["name"] == "Тип события": + rules = node["parameters"]["rules"]["values"] + for r in rules: + print(f"Branch '{r['outputKey']}':") + for c in r["conditions"]["conditions"]: + print(f" {c['leftValue']} {c['operator']['operation']} {c['rightValue']}")