Compare commits
6 commits
e57b811956
...
95b3866549
| Author | SHA1 | Date | |
|---|---|---|---|
| 95b3866549 | |||
| 111ffd266a | |||
| 88aa6a0798 | |||
| 79c4f6dde2 | |||
| 047330b384 | |||
| c8b45d537c |
6 changed files with 1370 additions and 402 deletions
773
deploy/n8n/genome-PR-review.json
Normal file
773
deploy/n8n/genome-PR-review.json
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
{
|
||||
"name": "Genome: PR review",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "forgejo-pr-review-forgejo-pr-review-23319ab8687b16f10e0f278fb920c112",
|
||||
"options": {}
|
||||
},
|
||||
"id": "edf8e431-3637-477d-83bd-1f077843f740",
|
||||
"name": "Webhook PR Review",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
-144,
|
||||
304
|
||||
],
|
||||
"webhookId": "61ff3a5baa304571"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// THE only parser of the review side: parse the directive, VALIDATE, prepare the rework payload.\n// Security: only allow-listed maintainers may drive the gate; destructive directives require a\n// feat/ai-ingest-* branch on the expected base; raw_source is recovered from a machine-readable\n// marker that run-ingest.sh writes into the PR body.\nconst ALLOWED_SENDERS = ['Keru']; // <-- maintainers allowed to issue directives\nconst BASE = 'develop';\nconst j = $json.body || $json;\nconst review = j.review || null, comment = j.comment || null;\nconst pr = j.pull_request || j.issue || null;\nconst body = ((review && review.content) || (comment && comment.body) || '').toString();\nconst sender = (j.sender && j.sender.login) || 'unknown';\n\nconst m = body.match(/^\\s*(REWORK|RESTART|REVERT\\s+\\d+|SPLIT|REJECT|MERGE)\\s*:?/i);\nif (!m) return { directive: 'NONE' };\nconst headTok = m[1].toUpperCase().replace(/\\s+/g, ' ');\nlet directive = headTok.startsWith('REVERT') ? 'REVERT' : headTok;\nconst feedback = body.slice(m[0].length).trim() || '(nessun dettaglio fornito)';\n\nconst prNumber = (pr && pr.number) || null;\nconst branch = (pr && pr.head && pr.head.ref) || null;\nconst base = (pr && pr.base && pr.base.ref) || null;\nconst repo = (pr && pr.base && pr.base.repo && pr.base.repo.name) || (j.repository && j.repository.name) || null;\nconst owner = (pr && pr.base && pr.base.repo && pr.base.repo.owner && pr.base.repo.owner.login)\n || (j.repository && j.repository.owner && j.repository.owner.login) || null;\nconst prBody = (pr && pr.body) || (j.issue && j.issue.body) || '';\nconst rawMatch = prBody.match(/<!--\\s*kg:raw=([^\\s]+)\\s*-->/);\nconst raw = rawMatch ? rawMatch[1] : null;\n\nif (directive === 'REVERT') return { directive: 'NONE', note: 'REVERT reserved for Step 7' };\nif (!ALLOWED_SENDERS.includes(sender))\n return { directive: 'UNAUTHORIZED', attempted: directive, sender, prNumber, owner, repo };\n\nconst okGenome = !!repo && /^[a-z0-9][a-z0-9-]{0,63}$/.test(repo);\nconst okPr = !!prNumber && /^[0-9]+$/.test(String(prNumber));\nconst okBranch = !!branch && /^feat\\/ai-ingest-[a-z0-9-]+$/.test(branch);\nconst okBase = base === BASE;\nconst okRaw = (directive === 'MERGE') ? true\n : (!!raw && raw.startsWith('raw/') && !raw.includes('..') && /^[A-Za-z0-9._\\/-]+$/.test(raw));\nif (!okGenome || !okPr || !okBase || (directive !== 'MERGE' && !okBranch) || !okRaw)\n return { directive: 'INVALID', attempted: directive, prNumber, owner, repo,\n why: { okGenome, okPr, okBranch, okBase, okRaw } };\n\nconst feedback_b64 = Buffer.from(feedback, 'utf8').toString('base64');\nreturn { directive, prNumber, branch, base, repo, owner, sender, raw, feedback, feedback_b64 };"
|
||||
},
|
||||
"id": "39977823-cbc1-45bb-b479-a57052b482e9",
|
||||
"name": "Parse & validate",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
80,
|
||||
304
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "MERGE",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "4960f0868bc54687"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "REWORK",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "34002fdd92834d38"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "RESTART",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "d412a74e32ac4f0c"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "SPLIT",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "c0810b33fa474ca0"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "REJECT",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "531039e699c44cea"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "UNAUTHORIZED",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "cfbd691d2e9a4c2a"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.directive }}",
|
||||
"rightValue": "INVALID",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "251f5b7beea6424a"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"fallbackOutput": "none"
|
||||
}
|
||||
},
|
||||
"id": "bc8aff39-a6bf-4e7c-8069-505d5855fb62",
|
||||
"name": "Switch",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
320,
|
||||
304
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://git.keruhomelab.com/api/v1/repos/{{ $('Parse & validate').first().json.owner }}/{{ $('Parse & validate').first().json.repo }}/pulls/{{ $('Parse & validate').first().json.prNumber }}/merge",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"Do\": \"merge\"\n}",
|
||||
"options": {
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"id": "2cea722f-42f1-475e-8060-7bac7cf4d245",
|
||||
"name": "Forgejo Merge PR",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
560,
|
||||
64
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cc369b5fc3d246a4",
|
||||
"leftValue": "={{ $('Parse & validate').first().json.branch }}",
|
||||
"rightValue": "feat/ai-ingest-",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "startsWith"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "aff153d5-48c8-4a31-bd37-5bce49e60fa9",
|
||||
"name": "Guardia feat/",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
560,
|
||||
288
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "=https://git.keruhomelab.com/api/v1/repos/{{ $('Parse & validate').first().json.owner }}/{{ $('Parse & validate').first().json.repo }}/pulls/{{ $('Parse & validate').first().json.prNumber }}",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"state\": \"closed\"\n}",
|
||||
"options": {
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"id": "1745f043-9dc3-44e2-8654-4cc88114d636",
|
||||
"name": "Forgejo Close PR",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
784,
|
||||
256
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "DELETE",
|
||||
"url": "=https://git.keruhomelab.com/api/v1/repos/{{ $('Parse & validate').first().json.owner }}/{{ $('Parse & validate').first().json.repo }}/branches/{{ encodeURIComponent($('Parse & validate').first().json.branch) }}",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpHeaderAuth",
|
||||
"options": {
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"id": "de59c610-c671-4a48-bca4-61ba9988bc65",
|
||||
"name": "Forgejo Delete Branch",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1008,
|
||||
256
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "55cf6c2a6c7d4d79",
|
||||
"leftValue": "={{ $('Parse & validate').first().json.directive }}",
|
||||
"rightValue": "REJECT",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "5c149f65-1ce2-4a39-9b86-aa05a993735c",
|
||||
"name": "E' REJECT?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
1232,
|
||||
256
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "24900f7e-959e-4398-8630-721a38443aa4",
|
||||
"name": "Power Manager - ensure-on",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.3,
|
||||
"position": [
|
||||
1232,
|
||||
128
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"workflowId": {
|
||||
"__rl": true,
|
||||
"value": "VIi2ovb5gJxNJLbg",
|
||||
"mode": "list",
|
||||
"cachedResultUrl": "/workflow/VIi2ovb5gJxNJLbg",
|
||||
"cachedResultName": "Genome: run-one-ingest"
|
||||
},
|
||||
"workflowInputs": {
|
||||
"mappingMode": "defineBelow",
|
||||
"value": {
|
||||
"genome": "={{ $('Parse & validate').first().json.repo }}",
|
||||
"raw": "={{ $('Parse & validate').first().json.raw }}",
|
||||
"mode": "rework",
|
||||
"feedback_b64": "={{ $('Parse & validate').first().json.feedback_b64 }}",
|
||||
"reason": "={{ $('Parse & validate').first().json.directive }}",
|
||||
"prevPr": "={{ $('Parse & validate').first().json.prNumber }}"
|
||||
},
|
||||
"matchingColumns": [],
|
||||
"schema": [
|
||||
{
|
||||
"id": "genome",
|
||||
"displayName": "genome",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "raw",
|
||||
"displayName": "raw",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "mode",
|
||||
"displayName": "mode",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "feedback_b64",
|
||||
"displayName": "feedback_b64",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "reason",
|
||||
"displayName": "reason",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "prevPr",
|
||||
"displayName": "prevPr",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"attemptToConvertTypes": false,
|
||||
"convertFieldsToString": true
|
||||
},
|
||||
"options": {
|
||||
"waitForSubWorkflow": false
|
||||
}
|
||||
},
|
||||
"id": "16774a78-b4eb-491f-9508-040aa3d4dc12",
|
||||
"name": "Run one ingest (rework)",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.3,
|
||||
"position": [
|
||||
1440,
|
||||
128
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// merged (MERGE) / closed (REJECT). The HTTP node replaced $json with the API response, so read\n// context from the parser (single review -> .first() is correct and pairedItem-proof).\nconst p = $('Parse & validate').first().json;\nconst repoUrl = `https://git.keruhomelab.com/${p.owner}/${p.repo}`;\nconst prUrl = `${repoUrl}/pulls/${p.prNumber}`;\nlet n;\nif (p.directive === 'MERGE') {\n n = { topic: 'genome-ingest', title: `${p.repo} \\u00b7 PR #${p.prNumber} mergiata`,\n priority: 'default', tags: 'twisted_rightwards_arrows', click: prUrl,\n actions: `view, Vedi la PR, ${prUrl}`,\n body: `PR #${p.prNumber} mergiata su \\`${p.base}\\` da **${p.sender}**.` };\n} else {\n n = { topic: 'genome-ingest', title: `${p.repo} \\u00b7 PR #${p.prNumber} chiusa`,\n priority: 'default', tags: 'wastebasket', click: repoUrl, actions: '',\n body: `**REJECT** di **${p.sender}**: PR #${p.prNumber} chiusa e branch \\`${p.branch}\\` rimosso. Nessun nuovo tentativo.\\n> ${p.feedback}` };\n}\nreturn n;"
|
||||
},
|
||||
"id": "f3c339cb-91e3-4436-b56a-b97c81d4a58f",
|
||||
"name": "Build ntfy action",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1440,
|
||||
304
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// Security / near-miss: unauthorized sender, invalid directive, or the feat/ guard. On all three\n// paths Switch/Guardia pass the parser output through, so $json carries the directive + context.\nconst d = $json;\nconst repoUrl = (d.owner && d.repo) ? `https://git.keruhomelab.com/${d.owner}/${d.repo}` : '';\nlet n;\nif (d.directive === 'UNAUTHORIZED') {\n n = { topic: 'genome-ingest', title: `Sicurezza \\u00b7 direttiva non autorizzata`,\n priority: 'high', tags: 'no_entry', click: repoUrl, actions: '',\n body: `**${d.sender}** ha tentato \\`${d.attempted}\\` su PR #${d.prNumber}, ma non \\u00e8 tra i maintainer autorizzati. **Nessuna azione** eseguita.` };\n} else if (d.directive === 'INVALID') {\n n = { topic: 'genome-ingest', title: `Direttiva non applicata`,\n priority: 'low', tags: 'information_source', click: repoUrl, actions: '',\n body: `\\`${d.attempted}\\` su PR #${d.prNumber} ignorata: precondizioni non soddisfatte (branch / base / marker raw).` };\n} else {\n n = { topic: 'genome-ingest', title: `Sicurezza \\u00b7 branch protetto`,\n priority: 'high', tags: 'no_entry', click: repoUrl, actions: '',\n body: `Rifiutata azione distruttiva (\\`${d.directive}\\`) sul branch \\`${d.branch}\\`: non \\u00e8 un \\`feat/ai-ingest-*\\`. **Nessuna modifica.**` };\n}\nreturn n;"
|
||||
},
|
||||
"id": "ba552761-b8cb-43c8-a6b1-ac93ca2b17b1",
|
||||
"name": "Build ntfy sicurezza",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
784,
|
||||
496
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "156f41ad-e1e1-4a7b-b91c-ceb2043ab147",
|
||||
"name": "ntfy: send",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1664,
|
||||
384
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
},
|
||||
"httpBearerAuth": {
|
||||
"id": "nCv4CUN7Ef086Ewj",
|
||||
"name": "Bearer Auth account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook PR Review": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse & validate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse & validate": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Switch",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Switch": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Forgejo Merge PR",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Power Manager - ensure-on",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Guardia feat/",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Guardia feat/",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Guardia feat/",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy sicurezza",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy sicurezza",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Forgejo Merge PR": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Guardia feat/": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Forgejo Close PR",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy sicurezza",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Forgejo Close PR": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Forgejo Delete Branch",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Forgejo Delete Branch": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "E' REJECT?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"E' REJECT?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Power Manager - ensure-on",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Power Manager - ensure-on": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Run one ingest (rework)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Build ntfy action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "ntfy: send",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Build ntfy sicurezza": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "ntfy: send",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate",
|
||||
"timeSavedMode": "fixed",
|
||||
"errorWorkflow": "7Vws3gCX3QnjM3oD",
|
||||
"callerPolicy": "workflowsFromSameOwner",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"versionId": "8c92ff1a-672a-4d15-9aa0-10d5fe11e472",
|
||||
"meta": {
|
||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||
},
|
||||
"id": "iho7kFQsXbGIxG7P",
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -7,48 +7,28 @@
|
|||
"path": "forgejo-push",
|
||||
"options": {}
|
||||
},
|
||||
"id": "eb4abf4a-d26d-4aea-85a2-fc356b81385f",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
1040,
|
||||
240
|
||||
1920,
|
||||
1728
|
||||
],
|
||||
"id": "9cc1b02e-6885-4a19-af34-ed2783ae99bf",
|
||||
"name": "Webhook",
|
||||
"webhookId": "bb518834-da85-46bb-bb72-97ba21a78faa"
|
||||
"webhookId": "cf215f5d31e04dd2"
|
||||
},
|
||||
{
|
||||
"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": {}
|
||||
"jsCode": "// Bell filter: proceed ONLY on develop pushes that actually touch raw/. Returning [] stops the\n// flow (no node needed). Performance: never wake vm101 for wiki-only pushes (e.g. an ingest PR\n// merged back to develop). pending-raw remains the source of truth.\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 commits = b.commits || [];\nconst touched = [];\nfor (const c of commits) {\n for (const p of (c.added || [])) touched.push(p);\n for (const p of (c.modified || [])) touched.push(p);\n for (const p of (c.removed || [])) touched.push(p);\n}\nif (!touched.some(p => p.startsWith('raw/'))) return [];\nreturn [{ json: { genome } }];"
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"id": "190d44ea-4f6f-4cff-91aa-3e65ef44cb21",
|
||||
"name": "Gate push",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1264,
|
||||
240
|
||||
],
|
||||
"id": "b2dd46aa-cdc3-4103-ad05-c728d9bd14ee",
|
||||
"name": "IF: ref == develop"
|
||||
2144,
|
||||
1728
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
|
|
@ -84,29 +64,28 @@
|
|||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "40af578c-eac4-47ac-9c30-5596eceaf9df",
|
||||
"name": "Power Manager - ensure-on",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.3,
|
||||
"position": [
|
||||
1488,
|
||||
240
|
||||
],
|
||||
"id": "e10f6af4-73ac-4689-b9f4-9c656d7c0cc4",
|
||||
"name": "Power Manager - ensure-on"
|
||||
2352,
|
||||
1728
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }}'"
|
||||
"command": "=ssh vm101 'pi pending-raw {{ $('Gate push').first().json.genome }}'"
|
||||
},
|
||||
"id": "f8861a50-aaf1-46fb-95a9-b9b200d4d6ae",
|
||||
"name": "SSH: pending-raw",
|
||||
"type": "n8n-nodes-base.ssh",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1712,
|
||||
240
|
||||
2576,
|
||||
1728
|
||||
],
|
||||
"id": "479d2e9d-0fde-417a-9122-d9780cc5dcba",
|
||||
"name": "SSH: changed-raw",
|
||||
"executeOnce": true,
|
||||
"credentials": {
|
||||
"sshPrivateKey": {
|
||||
"id": "GJQjKzte7Hjdfz89",
|
||||
|
|
@ -116,69 +95,166 @@
|
|||
},
|
||||
{
|
||||
"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;"
|
||||
"jsCode": "// Parse pending-raw -> one item per raw, carrying everything run-one-ingest needs. Unsafe\n// filenames (spaces / odd chars) are NOT ingested -> a 'badname' item -> ntfy.\nconst out = ($input.first().json.stdout || '').toString().trim();\nlet d;\ntry { d = JSON.parse(out); }\ncatch (e) { return [{ json: { _kind: 'error', reason: 'pending-raw non parsabile', raw: out } }]; }\nif (!d.files || d.files.length === 0) return [];\nconst why = {};\nfor (const it of (d.detail || [])) why[it.path] = it.reason;\nconst SAFE = /^[A-Za-z0-9._\\\\/-]+$/;\nconst items = [];\nfor (const raw of d.files) {\n if (SAFE.test(raw)) items.push({ json: { _kind: 'ingest', genome: d.genome, raw,\n mode: 'ingest', feedback_b64: '', reason: why[raw] || 'new', prevPr: '' } });\n else items.push({ json: { _kind: 'badname', genome: d.genome, raw,\n hint: raw.replace(/[^A-Za-z0-9._\\\\/-]+/g, '-').toLowerCase() } });\n}\nreturn items;"
|
||||
},
|
||||
"id": "e1f1e251-1565-4092-b8e7-b97c9c0bb18d",
|
||||
"name": "Split raw files",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1920,
|
||||
240
|
||||
],
|
||||
"id": "d540e454-4648-475c-8dce-5111ef876f75",
|
||||
"name": "Split raw files"
|
||||
2800,
|
||||
1728
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "privateKey",
|
||||
"command": "=ssh vm101 'pi ingest {{ $json.genome }} {{ $json.raw }}'"
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cbacf5d98d594ba5",
|
||||
"leftValue": "={{ $json._kind }}",
|
||||
"rightValue": "ingest",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.ssh",
|
||||
"typeVersion": 1,
|
||||
"id": "3339ce75-3ec9-4ed0-8faa-2433e9616c43",
|
||||
"name": "Nome valido?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
2144,
|
||||
240
|
||||
],
|
||||
"id": "7e30e055-7bc5-484a-a405-d29ea06ff175",
|
||||
"name": "SSH: pi ingest",
|
||||
"credentials": {
|
||||
"sshPrivateKey": {
|
||||
"id": "GJQjKzte7Hjdfz89",
|
||||
"name": "n8n container -> n8n-runner@nexus"
|
||||
3024,
|
||||
1728
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"workflowId": {
|
||||
"__rl": true,
|
||||
"value": "VIi2ovb5gJxNJLbg",
|
||||
"mode": "list",
|
||||
"cachedResultUrl": "/workflow/VIi2ovb5gJxNJLbg",
|
||||
"cachedResultName": "Genome: run-one-ingest"
|
||||
},
|
||||
"workflowInputs": {
|
||||
"mappingMode": "defineBelow",
|
||||
"value": {
|
||||
"genome": "={{ $json.genome }}",
|
||||
"raw": "={{ $json.raw }}",
|
||||
"mode": "ingest",
|
||||
"feedback_b64": "",
|
||||
"reason": "={{ $json.reason }}",
|
||||
"prevPr": ""
|
||||
},
|
||||
"matchingColumns": [],
|
||||
"schema": [
|
||||
{
|
||||
"id": "genome",
|
||||
"displayName": "genome",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "raw",
|
||||
"displayName": "raw",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "mode",
|
||||
"displayName": "mode",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "feedback_b64",
|
||||
"displayName": "feedback_b64",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "reason",
|
||||
"displayName": "reason",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
},
|
||||
{
|
||||
"id": "prevPr",
|
||||
"displayName": "prevPr",
|
||||
"required": false,
|
||||
"defaultMatch": false,
|
||||
"display": true,
|
||||
"canBeUsedToMatch": true,
|
||||
"type": "string",
|
||||
"removed": false
|
||||
}
|
||||
],
|
||||
"attemptToConvertTypes": false,
|
||||
"convertFieldsToString": true
|
||||
},
|
||||
"options": {
|
||||
"waitForSubWorkflow": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "fda796f5-588b-4502-a653-5d27c3f72ac6",
|
||||
"name": "Run one ingest",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.3,
|
||||
"position": [
|
||||
3232,
|
||||
1616
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 }; }"
|
||||
"jsCode": "const d = $json;\nreturn { topic: 'genome-ingest', title: `${d.genome} \\u00b7 file da rinominare`,\n priority: 'high', tags: 'warning', click: '', actions: '',\n body: `Il file \\`${d.raw}\\` ha spazi o caratteri non ammessi e **non** \\u00e8 stato ingerito.\\nRinominalo in: \\`${d.hint}\\`` };"
|
||||
},
|
||||
"id": "6820f3fb-97bb-45bf-8e7f-00eb68d7f313",
|
||||
"name": "Build ntfy badname",
|
||||
"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"
|
||||
3232,
|
||||
1840
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://ntfy/homelab-genome",
|
||||
"url": "=http://ntfy/{{ $json.topic }}",
|
||||
"authentication": "genericCredentialType",
|
||||
"genericAuthType": "httpBearerAuth",
|
||||
"sendHeaders": true,
|
||||
|
|
@ -195,6 +271,18 @@
|
|||
{
|
||||
"name": "Tags",
|
||||
"value": "={{ $json.tags }}"
|
||||
},
|
||||
{
|
||||
"name": "Click",
|
||||
"value": "={{ $json.click }}"
|
||||
},
|
||||
{
|
||||
"name": "Actions",
|
||||
"value": "={{ $json.actions }}"
|
||||
},
|
||||
{
|
||||
"name": "Markdown",
|
||||
"value": "yes"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -202,16 +290,18 @@
|
|||
"contentType": "raw",
|
||||
"rawContentType": "Raw / Text",
|
||||
"body": "={{ $json.body }}",
|
||||
"options": {}
|
||||
"options": {
|
||||
"timeout": 15000
|
||||
}
|
||||
},
|
||||
"id": "55cc42b2-3170-4884-bb5d-58e3af97bfea",
|
||||
"name": "ntfy: send",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
2800,
|
||||
240
|
||||
3456,
|
||||
1840
|
||||
],
|
||||
"id": "1f572cb3-741b-46bc-87fa-1e23ade5a9be",
|
||||
"name": "ntfy: send notification",
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
|
|
@ -222,53 +312,6 @@
|
|||
"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": {},
|
||||
|
|
@ -277,14 +320,14 @@
|
|||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IF: ref == develop",
|
||||
"node": "Gate push",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"IF: ref == develop": {
|
||||
"Gate push": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
|
|
@ -299,14 +342,14 @@
|
|||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "SSH: changed-raw",
|
||||
"node": "SSH: pending-raw",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"SSH: changed-raw": {
|
||||
"SSH: pending-raw": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
|
|
@ -321,51 +364,18 @@
|
|||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IF: nome valido",
|
||||
"node": "Nome valido?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"SSH: pi ingest": {
|
||||
"Nome valido?": {
|
||||
"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",
|
||||
"node": "Run one ingest",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
|
|
@ -383,7 +393,7 @@
|
|||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "ntfy: send notification",
|
||||
"node": "ntfy: send",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
|
|
@ -394,9 +404,13 @@
|
|||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate"
|
||||
"binaryMode": "separate",
|
||||
"timeSavedMode": "fixed",
|
||||
"errorWorkflow": "7Vws3gCX3QnjM3oD",
|
||||
"callerPolicy": "workflowsFromSameOwner",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"versionId": "2115dd9f-e2b6-4acb-8de0-4a166eb9729a",
|
||||
"versionId": "d58601e7-b752-4c9f-9438-d91be663c82e",
|
||||
"meta": {
|
||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||
},
|
||||
|
|
|
|||
128
deploy/n8n/genome-on-error.json
Normal file
128
deploy/n8n/genome-on-error.json
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"name": "Genome: on-error",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "eee467d7-5f8b-4abf-8923-1c70a29dafb2",
|
||||
"name": "Error Trigger",
|
||||
"type": "n8n-nodes-base.errorTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// Global error handler: set this workflow as the \"Error Workflow\" in each genome workflow's\n// Settings. Catches ANY node failure (SSH down, Forgejo 4xx/5xx, etc.) and notifies once.\nconst e = $json.execution || {};\nconst w = $json.workflow || {};\nconst msg = (e.error && (e.error.message || e.error.description)) || 'errore sconosciuto';\nconst lastNode = (e.lastNodeExecuted) ? ` (nodo: ${e.lastNodeExecuted})` : '';\nreturn { topic: 'genome-ingest', title: `Workflow KO \\u00b7 ${w.name || 'n8n'}`,\n priority: 'high', tags: 'rotating_light',\n click: e.url || '', actions: e.url ? `view, Apri l'esecuzione, ${e.url}` : '',\n body: `**${w.name || 'workflow'}** \\u00e8 fallito${lastNode}.\\n${msg}` };"
|
||||
},
|
||||
"id": "bdbf5186-143d-4482-b873-5760fbdabab0",
|
||||
"name": "Build ntfy",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
240,
|
||||
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": "16e9a3af-6acc-46f2-bc56-79e185fddf53",
|
||||
"name": "ntfy: send",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
464,
|
||||
0
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
},
|
||||
"httpBearerAuth": {
|
||||
"id": "nCv4CUN7Ef086Ewj",
|
||||
"name": "Bearer Auth account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Error Trigger": {
|
||||
"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": "95bfb02a-7122-43d7-bec6-3a2e5b77a469",
|
||||
"meta": {
|
||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||
},
|
||||
"id": "7Vws3gCX3QnjM3oD",
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
{
|
||||
"name": "Genome: raw → commit",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{
|
||||
"field": "cronExpression",
|
||||
"expression": "*/2 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.3,
|
||||
"position": [
|
||||
384,
|
||||
1056
|
||||
],
|
||||
"id": "520c79c8-76e6-41c0-8836-4d8d8f4ed236",
|
||||
"name": "Schedule: ogni 2 min"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "privateKey",
|
||||
"command": "sudo -u homelab -H /usr/local/bin/genome-raw-commit genome-test"
|
||||
},
|
||||
"type": "n8n-nodes-base.ssh",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
608,
|
||||
1056
|
||||
],
|
||||
"id": "fe89a85f-d63e-47d9-a7b4-08222f2635d0",
|
||||
"name": "SSH: genome-raw-commit",
|
||||
"executeOnce": true,
|
||||
"credentials": {
|
||||
"sshPrivateKey": {
|
||||
"id": "GJQjKzte7Hjdfz89",
|
||||
"name": "n8n container -> n8n-runner@nexus"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Lo script ora stampa JSON multilinea (jq -n). git manda i progressi su stderr,\n// quindi stdout e' SOLO il JSON: si parsa per intero.\nconst out = ($input.first().json.stdout || '').trim();\nlet data;\ntry {\n data = JSON.parse(out);\n} catch (e) {\n data = { status: 'error', reason: 'output non parsabile', genome: 'genome-test', raw: out };\n}\nreturn [{ json: data }];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
832,
|
||||
1056
|
||||
],
|
||||
"id": "74051cc5-5760-453d-80e4-0696d31bfc15",
|
||||
"name": "Parse result"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c0000000-0000-4000-8000-000000000001",
|
||||
"leftValue": "={{ $json.status }}",
|
||||
"rightValue": "noop",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "notEquals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
1056,
|
||||
1056
|
||||
],
|
||||
"id": "5813753d-f015-4a4e-b386-9d60659077c3",
|
||||
"name": "IF: non noop"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const d = $input.first().json;\nlet n;\nif (d.status === 'ok') {\n const f = d.files && d.files[0];\n n = {\n title: `Genome: ${d.commits} raw -> ${d.base}`,\n priority: 'default',\n tags: 'inbox_tray',\n body: `✅ ${d.genome}: ${d.commits} commit su ${d.base} (HEAD ${d.head})\\n\\n${d.summary || ''}`\n + (f ? `\\n\\n🔗 Forgejo: ${f.remote_url}\\n📂 Locale: ${f.local_url}` : '')\n };\n} else {\n n = {\n title: 'Genome raw commit: ERRORE',\n priority: 'high',\n tags: 'warning',\n body: `\\u274C ${d.genome || 'genome-test'}: ${d.reason || 'errore sconosciuto'}`\n };\n}\nreturn [{ json: n }];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1264,
|
||||
976
|
||||
],
|
||||
"id": "29eee748-4c2d-4e1e-8013-a64bc9cbf816",
|
||||
"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": [
|
||||
1488,
|
||||
976
|
||||
],
|
||||
"id": "d9b6ca21-59ef-44cf-a4f7-a75dcc7eeab4",
|
||||
"name": "ntfy: send notification",
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
},
|
||||
"httpBearerAuth": {
|
||||
"id": "nCv4CUN7Ef086Ewj",
|
||||
"name": "Bearer Auth account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Schedule: ogni 2 min": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "SSH: genome-raw-commit",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"SSH: genome-raw-commit": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IF: non noop",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"IF: non noop": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Build ntfy": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "ntfy: send notification",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate"
|
||||
},
|
||||
"versionId": "9607be0b-cd8c-4e7a-9ddb-63b6ec22b65d",
|
||||
"meta": {
|
||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||
},
|
||||
"id": "whyxMpvJMYQ55J1M",
|
||||
"tags": []
|
||||
}
|
||||
266
deploy/n8n/genome-run-one-ingest.json
Normal file
266
deploy/n8n/genome-run-one-ingest.json
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
{
|
||||
"name": "Genome: run-one-ingest",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"inputSource": "passthrough"
|
||||
},
|
||||
"id": "70da9144-1147-4cb5-9868-1f5ee2425d4c",
|
||||
"name": "On ingest request",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
-32,
|
||||
416
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// SECURITY chokepoint: every ingest to vm101 passes here. Re-validate inputs (defense in depth:\n// callers + the SSH wrapper also validate) and assemble the exact command. Charset-validated\n// fields are safe inside the single-quoted remote command -> no shell injection.\nconst d = $json;\nconst genome = (d.genome || '').toString();\nconst raw = (d.raw || '').toString();\nconst mode = (d.mode || 'ingest').toString();\nconst fb = (d.feedback_b64 || '').toString();\n\nconst okGenome = /^[a-z0-9][a-z0-9-]{0,63}$/.test(genome);\nconst okMode = (mode === 'ingest' || mode === 'rework');\nconst okRaw = raw.startsWith('raw/') && !raw.includes('..') && /^[A-Za-z0-9._\\/-]+$/.test(raw);\nconst okFb = (mode === 'ingest') || /^[A-Za-z0-9+/=]+$/.test(fb);\n\nif (!okGenome || !okMode || !okRaw || !okFb) {\n return { _ok: false, genome, mode,\n _reason: `bad input (genome:${okGenome} mode:${okMode} raw:${okRaw} fb:${okFb})` };\n}\nconst ssh_cmd = (mode === 'rework')\n ? `ssh vm101 'pi ingest-rework ${genome} ${raw} ${fb}'`\n : `ssh vm101 'pi ingest ${genome} ${raw}'`;\nreturn { _ok: true, ssh_cmd, genome, raw, mode, reason: d.reason || '', prevPr: d.prevPr || '' };"
|
||||
},
|
||||
"id": "551ec0f1-450c-41ce-88a1-8690bc2c1c0b",
|
||||
"name": "Guard & build cmd",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
192,
|
||||
416
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "4507e3a8b9714c7e",
|
||||
"leftValue": "={{ $json._ok }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "ea4c36ed-c452-406b-9c94-c58fdc69ed20",
|
||||
"name": "Input valido?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
416,
|
||||
416
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"authentication": "privateKey",
|
||||
"command": "={{ $json.ssh_cmd }}"
|
||||
},
|
||||
"id": "a5ea3f08-df3b-4433-a04e-b69ce742575f",
|
||||
"name": "SSH: ingest",
|
||||
"type": "n8n-nodes-base.ssh",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
624,
|
||||
336
|
||||
],
|
||||
"credentials": {
|
||||
"sshPrivateKey": {
|
||||
"id": "GJQjKzte7Hjdfz89",
|
||||
"name": "n8n container -> n8n-runner@nexus"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// run-ingest.sh prints one JSON line; the wrapper may instead print {status:busy|error,...}.\n// Take the last {...} line from stdout.\nconst out = ($json.stdout || '').toString().trim();\nconst line = out.split('\\n').filter(l => l.trim().startsWith('{')).pop();\nlet r;\ntry { r = line ? JSON.parse(line) : { status: 'error', reason: 'nessuna riga JSON', raw: out }; }\ncatch (e) { r = { status: 'error', reason: 'JSON non parsabile', raw: line }; }\nreturn r;"
|
||||
},
|
||||
"id": "0ee5e6c2-111a-4458-aab7-20a683f027ee",
|
||||
"name": "Parse result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
848,
|
||||
336
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// One builder for ingest + rework outcomes. Title is plain ASCII; the icon comes from Tags\n// (ntfy shortcodes); navigation is via Click (tap) + Actions (button) so it works on every\n// client. $('Guard...').item is reliable here: no executeWorkflow sits between Guard and here.\nconst g = $('Guard & build cmd').item.json;\nconst verb = (g.mode === 'rework') ? 'rework' : 'ingest';\nconst d = $json;\nlet n;\nif (g._ok === false) {\n n = { title: `Errore ${verb}: input non valido`, priority: 'high', tags: 'rotating_light',\n click: '', actions: '', body: `Richiesta di ${verb} rifiutata.\\n${g._reason}` };\n} else if (d.status === 'ok') {\n const pm = (d.pr_url || '').match(/\\/pulls\\/(\\d+)/);\n const num = pm ? `#${pm[1]}` : '';\n const lint = d.lint_clean ? 'lint pulito' : 'lint con avvisi';\n const conflict = d.conflict ? ' \\u00b7 \\u26a0\\ufe0f conflitto da risolvere' : '';\n n = { title: `${g.genome} \\u00b7 ${verb} ${d.slug} ${num}`.replace(/\\s+/g,' ').trim(),\n priority: d.conflict ? 'high' : 'default',\n tags: d.conflict ? 'warning' : 'white_check_mark',\n click: d.pr_url || '', actions: d.pr_url ? `view, Apri la PR, ${d.pr_url}` : '',\n body: `**${d.slug}** ${verb === 'rework' ? 'rilavorata' : 'ingerita'}`\n + (g.reason && verb === 'ingest' ? ` (${g.reason})` : '')\n + (g.prevPr ? ` \\u00b7 sostituisce #${g.prevPr}` : '')\n + `.\\n${lint}${conflict}.` };\n} else if (d.status === 'busy') {\n n = { title: `${g.genome} \\u00b7 ${verb} in coda`, priority: 'min', tags: 'hourglass_flowing_sand',\n click: '', actions: '',\n body: `Un altro ingest era in corso su questo genoma. La fonte resta pendente e verr\\u00e0 ripresa al prossimo campanello.` };\n} else if (d.status === 'pr_failed') {\n n = { title: `${g.genome} \\u00b7 ${d.slug}: PR non aperta`, priority: 'high', tags: 'warning',\n click: '', actions: '',\n body: `Semantic e lint ok, ma la PR non si \\u00e8 aperta.\\n${(d.detail || '').split('\\n')[0]}` };\n} else {\n const stage = d.stage ? ` (stage: ${d.stage})` : '';\n n = { title: `${g.genome} \\u00b7 errore ${verb}`, priority: 'high', tags: 'rotating_light',\n click: '', actions: '',\n body: `${(d.reason || 'errore')}${stage}.` + (d.log ? `\\nLog: ${d.log}` : '') };\n}\nn.topic = 'genome-ingest';\nreturn n;"
|
||||
},
|
||||
"id": "458318e5-7b26-4695-b564-f58c357d37d0",
|
||||
"name": "Build ntfy",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1072,
|
||||
416
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "4a1f0a89-1a56-4e1c-8fbc-173cba4ce97b",
|
||||
"name": "ntfy: send",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.4,
|
||||
"position": [
|
||||
1296,
|
||||
416
|
||||
],
|
||||
"credentials": {
|
||||
"httpHeaderAuth": {
|
||||
"id": "TBPXSWOF63k9mvm8",
|
||||
"name": "ntfy-token"
|
||||
},
|
||||
"httpBearerAuth": {
|
||||
"id": "nCv4CUN7Ef086Ewj",
|
||||
"name": "Bearer Auth account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"On ingest request": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Guard & build cmd",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Guard & build cmd": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Input valido?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Input valido?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "SSH: ingest",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Build ntfy",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"SSH: ingest": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Parse result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Parse result": {
|
||||
"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",
|
||||
"timeSavedMode": "fixed",
|
||||
"errorWorkflow": "7Vws3gCX3QnjM3oD",
|
||||
"callerPolicy": "workflowsFromSameOwner",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"versionId": "5d2cf4bd-f2c6-41fc-98a5-eaa797e31417",
|
||||
"meta": {
|
||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||
},
|
||||
"id": "VIi2ovb5gJxNJLbg",
|
||||
"tags": []
|
||||
}
|
||||
|
|
@ -60,6 +60,15 @@ with open(raw_rel, "r", encoding="utf-8") as fh:
|
|||
if not source_text.strip():
|
||||
die("preflight", "source is empty: " + raw_rel)
|
||||
|
||||
# --- pre-flight check: if the prompt exceeds context window, exit cleanly with stage:input ---
|
||||
# Conservative estimate: ~4 chars/token for mixed IT/EN text
|
||||
SAFETY_MARGIN = 4096 # room for system prompt + JSON response
|
||||
MAX_SOURCE_TOKENS = NUM_CTX - SAFETY_MARGIN
|
||||
MAX_SOURCE_CHARS = MAX_SOURCE_TOKENS * 4
|
||||
|
||||
if len(source_text) > MAX_SOURCE_CHARS:
|
||||
die("input", f"source too large ({len(source_text)} chars, limit ~{MAX_SOURCE_CHARS}). "
|
||||
f"Use the SPLIT directive or divide the document.")
|
||||
|
||||
# --- read existing index to avoid duplicate slugs ---
|
||||
existing_entities = set()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue