From 2e557ad48f6e75330095ebf94fe5a5ebf66eae0f Mon Sep 17 00:00:00 2001 From: Matteo Cherubini Date: Thu, 25 Jun 2026 12:59:29 +0200 Subject: [PATCH] feat(n8n): Implement automated Genome ingest workflow --- deploy/n8n/Genome_ ingest.json | 405 +++++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 deploy/n8n/Genome_ ingest.json diff --git a/deploy/n8n/Genome_ ingest.json b/deploy/n8n/Genome_ ingest.json new file mode 100644 index 0000000..142f3b4 --- /dev/null +++ b/deploy/n8n/Genome_ ingest.json @@ -0,0 +1,405 @@ +{ + "name": "Genome: ingest", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "forgejo-push", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 1040, + 240 + ], + "id": "9cc1b02e-6885-4a19-af34-ed2783ae99bf", + "name": "Webhook", + "webhookId": "bb518834-da85-46bb-bb72-97ba21a78faa" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "cc000000-0000-4000-8000-000000000001", + "leftValue": "={{ $json.body.ref }}", + "rightValue": "refs/heads/develop", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1264, + 240 + ], + "id": "b2dd46aa-cdc3-4103-ad05-c728d9bd14ee", + "name": "IF: ref == develop" + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "value": "zbtRXWsLt56nEIfz", + "mode": "list", + "cachedResultUrl": "/workflow/zbtRXWsLt56nEIfz", + "cachedResultName": "Power Manager" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "mode": "ensure-on" + }, + "matchingColumns": [ + "mode" + ], + "schema": [ + { + "id": "mode", + "displayName": "mode", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.3, + "position": [ + 1488, + 240 + ], + "id": "e10f6af4-73ac-4689-b9f4-9c656d7c0cc4", + "name": "Power Manager - ensure-on" + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=ssh vm101 'pi changed-raw {{ $('Webhook').item.json.body.repository.name }} {{ $('Webhook').item.json.body.before }} {{ $('Webhook').item.json.body.after }}'" + }, + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 1712, + 240 + ], + "id": "479d2e9d-0fde-417a-9122-d9780cc5dcba", + "name": "SSH: changed-raw", + "executeOnce": true, + "credentials": { + "sshPrivateKey": { + "id": "GJQjKzte7Hjdfz89", + "name": "n8n container -> n8n-runner@nexus" + } + } + }, + { + "parameters": { + "jsCode": "// run-once-for-all: parse changed-raw (JSON intero) -> 1 item per raw.\n// I nomi raw con spazi o caratteri non sicuri romperebbero il trasporto SSH\n// (lo spazio e' separatore di token nel comando 'pi ingest g raw'). Quindi qui\n// valido i nomi: quelli problematici NON vengono ingeriti, ma emettono un item\n// di tipo 'badname' che a valle diventa un ntfy 'rinomina il file'.\nconst out = ($input.first().json.stdout || '').toString().trim();\nlet d;\ntry { d = JSON.parse(out); }\ncatch (e) { return [{ json: { _kind: 'error', reason: 'changed-raw non parsabile', raw: out } }]; }\nif (!d.files || d.files.length === 0) return []; // niente raw -> stop silenzioso\n\n// regola 'non rompicoglioni': consentiti lettere, numeri, punto, slash, trattino, underscore.\n// VIETATI: spazi e tutto il resto (che spezzano SSH o gli slug downstream).\nconst SAFE = /^[A-Za-z0-9._\\/-]+$/;\nconst out_items = [];\nfor (const raw of d.files) {\n if (SAFE.test(raw)) {\n out_items.push({ json: { _kind: 'ingest', genome: d.genome, raw } });\n } else {\n out_items.push({ json: { _kind: 'badname', genome: d.genome, raw,\n hint: raw.replace(/[^A-Za-z0-9._\\/-]+/g, '-').toLowerCase() } });\n }\n}\nreturn out_items;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1920, + 240 + ], + "id": "d540e454-4648-475c-8dce-5111ef876f75", + "name": "Split raw files" + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=ssh vm101 'pi ingest {{ $json.genome }} {{ $json.raw }}'" + }, + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 2144, + 240 + ], + "id": "7e30e055-7bc5-484a-a405-d29ea06ff175", + "name": "SSH: pi ingest", + "credentials": { + "sshPrivateKey": { + "id": "GJQjKzte7Hjdfz89", + "name": "n8n container -> n8n-runner@nexus" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "// per-item: ultima riga JSON di run-ingest.sh\nconst out = ($json.stdout || '').trim();\nconst line = out.split('\\n').filter(l => l.trim().startsWith('{')).pop();\nif (!line) return { status: 'error', reason: 'nessuna riga JSON run-ingest', raw: out };\ntry { return JSON.parse(line); } catch (e) { return { status: 'error', reason: 'JSON non parsabile', raw: line }; }" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2368, + 240 + ], + "id": "f60878f4-8cca-43d0-b8b3-0aa1a422237b", + "name": "Parse ingest" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const d = $json;\nlet n;\nif (d.status === 'ok') {\n n = { title: `Ingest ${d.slug}: PR aperta`, priority: 'default', tags: 'inbox_tray',\n body: `\\u2705 ${d.slug}: PR aperta (lint ${d.lint_clean ? 'clean' : 'KO'}${d.conflict ? ', CONFLITTO' : ''})\\n\\n\\ud83d\\udd17 ${d.pr_url}` };\n} else if (d.status === 'pr_failed') {\n n = { title: `Ingest ${d.slug}: PR FALLITA`, priority: 'high', tags: 'warning',\n body: `\\u26a0\\ufe0f ${d.slug}: semantic/lint ok ma PR non aperta.\\n\\n${(d.detail || '').split('\\n')[0]}` };\n} else {\n n = { title: 'Ingest: ERRORE', priority: 'high', tags: 'rotating_light',\n body: `\\u274c ${d.reason || 'errore'}\\n\\n${(d.raw || '').slice(0,300)}` };\n}\nreturn n;" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2592, + 240 + ], + "id": "9018dff6-b314-4ca8-b8ff-fd5423818816", + "name": "Build ntfy" + }, + { + "parameters": { + "method": "POST", + "url": "http://ntfy/homelab-genome", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Title", + "value": "={{ $json.title }}" + }, + { + "name": "Priority", + "value": "={{ $json.priority }}" + }, + { + "name": "Tags", + "value": "={{ $json.tags }}" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "Raw / Text", + "body": "={{ $json.body }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 2800, + 240 + ], + "id": "1f572cb3-741b-46bc-87fa-1e23ade5a9be", + "name": "ntfy: send notification", + "credentials": { + "httpHeaderAuth": { + "id": "TBPXSWOF63k9mvm8", + "name": "ntfy-token" + }, + "httpBearerAuth": { + "id": "nCv4CUN7Ef086Ewj", + "name": "Bearer Auth account" + } + } + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "dd000000-0000-4000-8000-000000000001", + "leftValue": "={{ $json._kind }}", + "rightValue": "ingest", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 3168, + 240 + ], + "id": "d2d2e2b2-9bd7-446b-b6b8-0a865d49c601", + "name": "IF: nome valido" + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const d=$json;\nreturn {\n title: 'Ingest: nome file non valido',\n priority: 'high',\n tags: 'warning',\n body: `\\u26a0\\ufe0f \"${d.raw}\" ha spazi o caratteri non ammessi e non e' stato ingerito.\\n\\nRinominalo in: ${d.hint}`\n};" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3392, + 400 + ], + "id": "3b69aa97-170a-4666-8b0e-4b51b48b2817", + "name": "Build ntfy badname" + } + ], + "pinData": {}, + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "IF: ref == develop", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF: ref == develop": { + "main": [ + [ + { + "node": "Power Manager - ensure-on", + "type": "main", + "index": 0 + } + ] + ] + }, + "Power Manager - ensure-on": { + "main": [ + [ + { + "node": "SSH: changed-raw", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH: changed-raw": { + "main": [ + [ + { + "node": "Split raw files", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split raw files": { + "main": [ + [ + { + "node": "IF: nome valido", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH: pi ingest": { + "main": [ + [ + { + "node": "Parse ingest", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse ingest": { + "main": [ + [ + { + "node": "Build ntfy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build ntfy": { + "main": [ + [ + { + "node": "ntfy: send notification", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF: nome valido": { + "main": [ + [ + { + "node": "SSH: pi ingest", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build ntfy badname", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build ntfy badname": { + "main": [ + [ + { + "node": "ntfy: send notification", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "2115dd9f-e2b6-4acb-8de0-4a166eb9729a", + "meta": { + "instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417" + }, + "id": "mUJUuQxcDiiPWcUE", + "tags": [] +} \ No newline at end of file