From c4aba8507cbee4913b53e8d4bd1f66fe3a7b423f Mon Sep 17 00:00:00 2001 From: Matteo Cherubini Date: Wed, 1 Jul 2026 19:53:57 +0200 Subject: [PATCH] feat(n8n): add workflow for genome content pruning on raw/ file deletion --- deploy/n8n/genome-prune.json | 322 +++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 deploy/n8n/genome-prune.json diff --git a/deploy/n8n/genome-prune.json b/deploy/n8n/genome-prune.json new file mode 100644 index 0000000..670676b --- /dev/null +++ b/deploy/n8n/genome-prune.json @@ -0,0 +1,322 @@ +{ + "name": "Genome: prune", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "forgejo-push-prune", + "options": {} + }, + "id": "050b318b-d3bb-4c1a-baa4-a1e2bd1babd8", + "name": "Webhook prune", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + 0, + 0 + ], + "webhookId": "d6ac11900058434e" + }, + { + "parameters": { + "jsCode": "// Bell filter for PRUNING: proceed only on develop pushes that REMOVED a raw/ file.\n// Adds/modifications are the ingest flow's job; this flow reacts to deletions only.\nconst b = $json.body || $json;\nconst ref = b.ref || '';\nconst genome = (b.repository && b.repository.name) || '';\nif (ref !== 'refs/heads/develop') return [];\nif (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(genome)) return [];\nconst removed = [];\nfor (const c of (b.commits || [])) for (const p of (c.removed || [])) removed.push(p);\nif (!removed.some(p => p.startsWith('raw/'))) return []; // nothing under raw/ removed -> ignore\nreturn [{ json: { genome } }];" + }, + "id": "deac740a-2046-4de8-91eb-812114edeb7b", + "name": "Gate prune", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 224, + 0 + ] + }, + { + "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": {} + }, + "id": "554737ea-09f3-4d4b-9a66-c983cc72e655", + "name": "Power Manager - ensure-on", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.3, + "position": [ + 448, + 0 + ] + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=ssh vm101 'pi orphan-wiki {{ $('Gate prune').first().json.genome }}'" + }, + "id": "311005a4-16fd-4752-92d3-e3bbb9cdf19f", + "name": "SSH: orphan-wiki", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 672, + 0 + ], + "credentials": { + "sshPrivateKey": { + "id": "GJQjKzte7Hjdfz89", + "name": "n8n container -> n8n-runner@nexus" + } + } + }, + { + "parameters": { + "jsCode": "// Gate: prune only if orphan-wiki found orphans. run-prune re-derives independently anyway\n// (no detected-vs-pruned race) — this just avoids taking the lock for nothing.\nconst out = ($input.first().json.stdout || '').toString().trim();\nlet d; try { d = JSON.parse(out); } catch (e) { return []; }\nif (!d || !d.count) return []; // 0 orphans -> stop silently\nreturn [{ json: { genome: d.genome, count: d.count } }];" + }, + "id": "2374b63c-f0db-4ae9-b350-3fec70687384", + "name": "Orfani?", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 880, + 0 + ] + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=ssh vm101 'pi prune {{ $json.genome }}'" + }, + "id": "e4130173-ff62-4e11-b3d1-ee7870803663", + "name": "SSH: prune", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 1104, + 0 + ], + "credentials": { + "sshPrivateKey": { + "id": "GJQjKzte7Hjdfz89", + "name": "n8n container -> n8n-runner@nexus" + } + } + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "const out = ($json.stdout || '').toString().trim();\nconst line = out.split('\\n').filter(l => l.trim().startsWith('{')).pop();\nlet r; try { r = line ? JSON.parse(line) : { status:'error', reason:'nessuna riga JSON' }; }\ncatch (e) { r = { status:'error', reason:'JSON non parsabile' }; }\nreturn r;" + }, + "id": "b649df9a-1e64-49cc-9e7f-6f78a1190382", + "name": "Parse prune", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1328, + 0 + ] + }, + { + "parameters": { + "mode": "runOnceForEachItem", + "jsCode": "// Pruning notification on genome-ingest (the \"system produced a PR to judge\" topic), broom icon.\nconst d = $json;\nconst g = $('Orfani?').first().json;\nlet n;\nif (d.status === 'ok') {\n const pm = (d.pr_url || '').match(/\\/pulls\\/(\\d+)/); const num = pm ? `#${pm[1]}` : '';\n n = { topic:'genome-ingest', title:`${g.genome} \\u00b7 potatura ${num}`.replace(/\\s+/g,' ').trim(),\n priority:'default', tags:'broom', click:d.pr_url || '', actions:d.pr_url ? `view, Apri la PR, ${d.pr_url}` : '',\n body:`${d.count} sorgente/i orfane proposte per la rimozione. **Approva la PR** per potare, oppure chiudila da Forgejo per annullare.` };\n} else {\n n = { topic:'genome-ingest', title:`${g ? g.genome : ''} \\u00b7 errore potatura`.trim(), priority:'high',\n tags:'rotating_light', click:'', actions:'', body:`${d.reason || 'errore'}.` };\n}\nreturn n;" + }, + "id": "00be2a1f-6097-40e2-81f5-9dcba40b66ae", + "name": "Build ntfy", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1552, + 0 + ] + }, + { + "parameters": { + "method": "POST", + "url": "=http://ntfy/{{ $json.topic }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpBearerAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Title", + "value": "={{ $json.title }}" + }, + { + "name": "Priority", + "value": "={{ $json.priority }}" + }, + { + "name": "Tags", + "value": "={{ $json.tags }}" + }, + { + "name": "Click", + "value": "={{ $json.click }}" + }, + { + "name": "Actions", + "value": "={{ $json.actions }}" + }, + { + "name": "Markdown", + "value": "yes" + } + ] + }, + "sendBody": true, + "contentType": "raw", + "rawContentType": "Raw / Text", + "body": "={{ $json.body }}", + "options": { + "timeout": 15000 + } + }, + "id": "50a36840-be50-43ec-bea9-44819a88a923", + "name": "ntfy: send", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.4, + "position": [ + 1760, + 0 + ], + "credentials": { + "httpHeaderAuth": { + "id": "TBPXSWOF63k9mvm8", + "name": "ntfy-token" + }, + "httpBearerAuth": { + "id": "nCv4CUN7Ef086Ewj", + "name": "Bearer Auth account" + } + } + } + ], + "pinData": {}, + "connections": { + "Webhook prune": { + "main": [ + [ + { + "node": "Gate prune", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gate prune": { + "main": [ + [ + { + "node": "Power Manager - ensure-on", + "type": "main", + "index": 0 + } + ] + ] + }, + "Power Manager - ensure-on": { + "main": [ + [ + { + "node": "SSH: orphan-wiki", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH: orphan-wiki": { + "main": [ + [ + { + "node": "Orfani?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Orfani?": { + "main": [ + [ + { + "node": "SSH: prune", + "type": "main", + "index": 0 + } + ] + ] + }, + "SSH: prune": { + "main": [ + [ + { + "node": "Parse prune", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse prune": { + "main": [ + [ + { + "node": "Build ntfy", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build ntfy": { + "main": [ + [ + { + "node": "ntfy: send", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1", + "binaryMode": "separate" + }, + "versionId": "ff0be89b-7930-4171-a547-5dc7bffc9472", + "meta": { + "instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417" + }, + "id": "smH5Qrv7CQnTtdAF", + "tags": [] +} \ No newline at end of file