Merge branch 'release/1.13.0' into main
This commit is contained in:
commit
06a16f1e81
6 changed files with 141 additions and 137 deletions
2
Makefile
2
Makefile
|
|
@ -1,5 +1,5 @@
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Knowledge Genome - Makefile v. 1.12.0
|
# Knowledge Genome - Makefile v. 1.13.0
|
||||||
# Orchestrates the setup and management of the knowledge base.
|
# Orchestrates the setup and management of the knowledge base.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,31 +4,31 @@
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"httpMethod": "POST",
|
"httpMethod": "POST",
|
||||||
"path": "forgejo-pr-review-forgejo-pr-review-23319ab8687b16f10e0f278fb920c112",
|
"path": "forgejo-pr-review-23319ab8687b16f10e0f278fb920c112",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "edf8e431-3637-477d-83bd-1f077843f740",
|
"id": "58df1ca9-e48e-4834-b231-d97c974cd01b",
|
||||||
"name": "Webhook PR Review",
|
"name": "Webhook PR Review",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 2.1,
|
"typeVersion": 2.1,
|
||||||
"position": [
|
"position": [
|
||||||
-144,
|
2272,
|
||||||
304
|
1344
|
||||||
],
|
],
|
||||||
"webhookId": "61ff3a5baa304571"
|
"webhookId": "61ff3a5baa304571"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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 };"
|
"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';\n\n// n8n Run Once for Each Item: $json is the current webhook payload\nconst j = $json.body || $json;\nif (!j || typeof j !== 'object') {\n return { directive: 'INVALID', reason: 'malformed webhook payload' };\n}\n\nconst review = j.review || null;\nconst comment = j.comment || null;\nconst pr = j.pull_request || j.issue || null;\n\n// Extract directive text from review content or comment body\nconst body = String(\n (review && review.content) ||\n (comment && comment.body) ||\n ''\n);\nconst sender = String((j.sender && j.sender.login) || 'unknown');\n\n// Match directive at the start of the text (case-insensitive)\nconst m = body.match(/^\\s*(REWORK|RESTART|REVERT\\s+\\d+|SPLIT|REJECT|MERGE)\\s*:?/i);\nif (!m) return { directive: 'NONE' };\n\nconst headTok = m[1].toUpperCase().replace(/\\s+/g, ' ');\nconst directive = headTok.startsWith('REVERT') ? 'REVERT' : headTok;\nconst feedback = body.slice(m[0].length).trim() || '(nessun dettaglio fornito)';\n\n// Extract PR metadata safely\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) ||\n (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) || '';\n\n// Recover raw_source from machine-readable marker: <!-- kg:raw=path -->\n// Restricted to valid path characters, no spaces, no HTML breaking\nconst rawMatch = prBody.match(/<!--\\s*kg:raw=([^\\s>]+)\\s*-->/);\nconst raw = rawMatch ? rawMatch[1] : null;\n\n// REVERT is reserved for future Step 7 implementation\nif (directive === 'REVERT') {\n return { directive: 'NONE', note: 'REVERT reserved for Step 7' };\n}\n\n// Authorization gate\nif (!ALLOWED_SENDERS.includes(sender)) {\n return {\n directive: 'UNAUTHORIZED',\n attempted: directive,\n sender,\n prNumber,\n owner,\n repo\n };\n}\n\n// Validation rules\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')\n ? true\n : (!!raw && raw.startsWith('raw/') && !raw.includes('..') && /^[A-Za-z0-9._\\/-]+$/.test(raw));\n\nif (!okGenome || !okPr || !okBase || (directive !== 'MERGE' && !okBranch) || !okRaw) {\n return {\n directive: 'INVALID',\n attempted: directive,\n prNumber,\n owner,\n repo,\n why: { okGenome, okPr, okBranch, okBase, okRaw }\n };\n}\n\n// Encode feedback for safe transport through SSH/scripts\nconst feedback_b64 = Buffer.from(feedback, 'utf8').toString('base64');\n\nreturn {\n directive,\n prNumber,\n branch,\n base,\n repo,\n owner,\n sender,\n raw,\n feedback,\n feedback_b64\n};"
|
||||||
},
|
},
|
||||||
"id": "39977823-cbc1-45bb-b479-a57052b482e9",
|
"id": "c668f595-0a28-4bd3-9125-22fee9350d78",
|
||||||
"name": "Parse & validate",
|
"name": "Parse & validate",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
80,
|
2496,
|
||||||
304
|
1344
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -188,13 +188,13 @@
|
||||||
"fallbackOutput": "none"
|
"fallbackOutput": "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "bc8aff39-a6bf-4e7c-8069-505d5855fb62",
|
"id": "489736cc-bab6-4664-8087-91b6d9ff31ad",
|
||||||
"name": "Switch",
|
"name": "Switch",
|
||||||
"type": "n8n-nodes-base.switch",
|
"type": "n8n-nodes-base.switch",
|
||||||
"typeVersion": 3.4,
|
"typeVersion": 3.4,
|
||||||
"position": [
|
"position": [
|
||||||
320,
|
2736,
|
||||||
304
|
1344
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -210,13 +210,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "2cea722f-42f1-475e-8060-7bac7cf4d245",
|
"id": "3440cb8d-ae4c-4523-ae13-ee5667d24252",
|
||||||
"name": "Forgejo Merge PR",
|
"name": "Forgejo Merge PR",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
560,
|
2976,
|
||||||
64
|
1104
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -248,13 +248,13 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "aff153d5-48c8-4a31-bd37-5bce49e60fa9",
|
"id": "e6d45fce-83d0-44ca-9fa4-86558fec1a0f",
|
||||||
"name": "Guardia feat/",
|
"name": "Guardia feat/",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.if",
|
||||||
"typeVersion": 2.2,
|
"typeVersion": 2.2,
|
||||||
"position": [
|
"position": [
|
||||||
560,
|
2976,
|
||||||
288
|
1328
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -270,13 +270,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "1745f043-9dc3-44e2-8654-4cc88114d636",
|
"id": "1601f705-c758-4df6-a3bd-e3ac2e202c94",
|
||||||
"name": "Forgejo Close PR",
|
"name": "Forgejo Close PR",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
784,
|
3200,
|
||||||
256
|
1296
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -295,13 +295,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "de59c610-c671-4a48-bca4-61ba9988bc65",
|
"id": "c2ff2247-efe1-4809-a435-9973188d61bb",
|
||||||
"name": "Forgejo Delete Branch",
|
"name": "Forgejo Delete Branch",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
1008,
|
3424,
|
||||||
256
|
1296
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -334,13 +334,13 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "5c149f65-1ce2-4a39-9b86-aa05a993735c",
|
"id": "a1dbbc06-555d-4a1d-8fbf-ee75f617e98a",
|
||||||
"name": "E' REJECT?",
|
"name": "E' REJECT?",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.if",
|
||||||
"typeVersion": 2.2,
|
"typeVersion": 2.2,
|
||||||
"position": [
|
"position": [
|
||||||
1232,
|
3648,
|
||||||
256
|
1296
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -377,13 +377,13 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "24900f7e-959e-4398-8630-721a38443aa4",
|
"id": "7fc3e648-4712-4eef-a6f3-12c8805ade1f",
|
||||||
"name": "Power Manager - ensure-on",
|
"name": "Power Manager - ensure-on",
|
||||||
"type": "n8n-nodes-base.executeWorkflow",
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
"typeVersion": 1.3,
|
"typeVersion": 1.3,
|
||||||
"position": [
|
"position": [
|
||||||
1232,
|
3648,
|
||||||
128
|
1168
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -403,7 +403,7 @@
|
||||||
"mode": "rework",
|
"mode": "rework",
|
||||||
"feedback_b64": "={{ $('Parse & validate').first().json.feedback_b64 }}",
|
"feedback_b64": "={{ $('Parse & validate').first().json.feedback_b64 }}",
|
||||||
"reason": "={{ $('Parse & validate').first().json.directive }}",
|
"reason": "={{ $('Parse & validate').first().json.directive }}",
|
||||||
"prevPr": "={{ $('Parse & validate').first().json.prNumber }}"
|
"prevPr": "={{ String($('Parse & validate').first().json.prNumber || '') }}"
|
||||||
},
|
},
|
||||||
"matchingColumns": [],
|
"matchingColumns": [],
|
||||||
"schema": [
|
"schema": [
|
||||||
|
|
@ -475,41 +475,41 @@
|
||||||
"waitForSubWorkflow": false
|
"waitForSubWorkflow": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "16774a78-b4eb-491f-9508-040aa3d4dc12",
|
"id": "9704c050-5c63-49fd-a26d-efbae9d92175",
|
||||||
"name": "Run one ingest (rework)",
|
"name": "Run one ingest (rework)",
|
||||||
"type": "n8n-nodes-base.executeWorkflow",
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
"typeVersion": 1.3,
|
"typeVersion": 1.3,
|
||||||
"position": [
|
"position": [
|
||||||
1440,
|
3856,
|
||||||
128
|
1168
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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;"
|
"jsCode": "// merged (MERGE) / closed (REJECT). The HTTP node replaced $json with the API response,\n// so we read context from the parser via node reference (single review -> .first() is safe).\n// Fallback values prevent crashes if the parser node is unreachable.\nconst p = $('Parse & validate').first().json || {};\nconst repo = p.repo || 'unknown';\nconst owner = p.owner || 'unknown';\nconst prNumber = p.prNumber || '?';\nconst base = p.base || 'develop';\nconst branch = p.branch || 'unknown';\nconst sender = p.sender || 'unknown';\nconst directive = p.directive || 'UNKNOWN';\nconst feedback = p.feedback || '';\n\nconst repoUrl = (owner && repo && repo !== 'unknown')\n ? `https://git.keruhomelab.com/${owner}/${repo}`\n : '';\nconst prUrl = (repoUrl && prNumber !== '?')\n ? `${repoUrl}/pulls/${prNumber}`\n : '';\n\nlet n;\nif (directive === 'MERGE') {\n n = {\n topic: 'genome-ingest',\n title: `${repo} · PR #${prNumber} mergiata`,\n priority: 'default',\n tags: 'twisted_rightwards_arrows',\n click: prUrl,\n actions: `view, Vedi la PR, ${prUrl}`,\n body: `PR #${prNumber} mergiata su \\`${base}\\` da **${sender}**.`\n };\n} else {\n n = {\n topic: 'genome-ingest',\n title: `${repo} · PR #${prNumber} chiusa`,\n priority: 'default',\n tags: 'wastebasket',\n click: repoUrl,\n actions: '',\n body: `**REJECT** di **${sender}**: PR #${prNumber} chiusa e branch \\`${branch}\\` rimosso. Nessun nuovo tentativo.\\n> ${feedback}`\n };\n}\n\nreturn n;"
|
||||||
},
|
},
|
||||||
"id": "f3c339cb-91e3-4436-b56a-b97c81d4a58f",
|
"id": "1ce634fd-d402-4a84-9ba1-04673ddffce9",
|
||||||
"name": "Build ntfy action",
|
"name": "Build ntfy action",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1440,
|
3856,
|
||||||
304
|
1344
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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;"
|
"jsCode": "// Security / near-miss: unauthorized sender, invalid directive, or the feat/ guard.\n// On all three paths Switch/Guardia pass the parser output through, so $json carries the directive + context.\nconst d = $json || {};\nconst directive = d.directive || 'UNKNOWN';\nconst attempted = d.attempted || directive;\nconst sender = d.sender || 'unknown';\nconst prNumber = d.prNumber || '?';\nconst branch = d.branch || 'unknown';\nconst owner = d.owner || '';\nconst repo = d.repo || '';\n\nconst repoUrl = (owner && repo) ? `https://git.keruhomelab.com/${owner}/${repo}` : '';\n\nlet n;\nif (directive === 'UNAUTHORIZED') {\n n = {\n topic: 'genome-ingest',\n title: `Sicurezza · direttiva non autorizzata`,\n priority: 'high',\n tags: 'no_entry',\n click: repoUrl,\n actions: '',\n body: `**${sender}** ha tentato \\`${attempted}\\` su PR #${prNumber}, ma non è tra i maintainer autorizzati. **Nessuna azione** eseguita.`\n };\n} else if (directive === 'INVALID') {\n n = {\n topic: 'genome-ingest',\n title: `Direttiva non applicata`,\n priority: 'low',\n tags: 'information_source',\n click: repoUrl,\n actions: '',\n body: `\\`${attempted}\\` su PR #${prNumber} ignorata: precondizioni non soddisfatte (branch / base / marker raw).`\n };\n} else {\n // Guardia feat/ false branch: destructive action on a non-feat/ai-ingest-* branch\n n = {\n topic: 'genome-ingest',\n title: `Sicurezza · branch protetto`,\n priority: 'high',\n tags: 'no_entry',\n click: repoUrl,\n actions: '',\n body: `Rifiutata azione distruttiva (\\`${attempted || directive}\\`) sul branch \\`${branch}\\`: non è un \\`feat/ai-ingest-*\\`. **Nessuna modifica.**`\n };\n}\n\nreturn n;"
|
||||||
},
|
},
|
||||||
"id": "ba552761-b8cb-43c8-a6b1-ac93ca2b17b1",
|
"id": "32b16592-5126-4cc2-a3f2-d1bda58ac724",
|
||||||
"name": "Build ntfy sicurezza",
|
"name": "Build ntfy sicurezza",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
784,
|
3200,
|
||||||
496
|
1536
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -555,13 +555,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "156f41ad-e1e1-4a7b-b91c-ceb2043ab147",
|
"id": "4d45b486-de42-4c7f-be21-b5bfbc05fd44",
|
||||||
"name": "ntfy: send",
|
"name": "ntfy: send",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
1664,
|
4080,
|
||||||
384
|
1424
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -764,7 +764,7 @@
|
||||||
"callerPolicy": "workflowsFromSameOwner",
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
"availableInMCP": false
|
"availableInMCP": false
|
||||||
},
|
},
|
||||||
"versionId": "8c92ff1a-672a-4d15-9aa0-10d5fe11e472",
|
"versionId": "22998a54-cd9a-4b57-9c80-df97085a997c",
|
||||||
"meta": {
|
"meta": {
|
||||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,26 @@
|
||||||
"path": "forgejo-push",
|
"path": "forgejo-push",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "eb4abf4a-d26d-4aea-85a2-fc356b81385f",
|
"id": "8c44b478-1a95-4c3b-8ac1-d7c57e228414",
|
||||||
"name": "Webhook",
|
"name": "Webhook",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 2.1,
|
"typeVersion": 2.1,
|
||||||
"position": [
|
"position": [
|
||||||
1920,
|
1520,
|
||||||
1728
|
1728
|
||||||
],
|
],
|
||||||
"webhookId": "cf215f5d31e04dd2"
|
"webhookId": "cf215f5d31e04dd2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"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 } }];"
|
"jsCode": "// Bell filter: proceed ONLY on develop pushes that actually touch raw/.\n// Returning [] stops the flow (no node needed).\n// Performance: never wake vm101 for wiki-only pushes (e.g. an ingest PR merged back to develop).\n// pending-raw remains the source of truth.\nconst item = $input.first().json;\nconst b = item.body || item;\nconst ref = String(b.ref || '');\nconst genome = String((b.repository && b.repository.name) || '').toLowerCase().trim();\n\n// Branch filter\nif (ref !== 'refs/heads/develop') return [];\n\n// Genome name validation (DNS-like: lowercase alphanum + hyphen, 1-64 chars)\nif (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(genome)) return [];\n\n// Collect all touched paths safely (added, modified, removed)\nconst commits = Array.isArray(b.commits) ? b.commits : [];\nconst touched = [];\nfor (const c of commits) {\n if (!c || typeof c !== 'object') continue;\n for (const key of ['added', 'modified', 'removed']) {\n const list = c[key];\n if (!Array.isArray(list)) continue;\n for (const p of list) {\n if (typeof p === 'string' && p.startsWith('raw/')) {\n touched.push(p);\n }\n }\n }\n}\n\n// Gate: stop if nothing under raw/ was touched\nif (touched.length === 0) return [];\n\nreturn [{ json: { genome, touchedCount: touched.length } }];"
|
||||||
},
|
},
|
||||||
"id": "190d44ea-4f6f-4cff-91aa-3e65ef44cb21",
|
"id": "604787c7-4e83-468e-9a98-3ac084203040",
|
||||||
"name": "Gate push",
|
"name": "Gate push",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
2144,
|
1744,
|
||||||
1728
|
1728
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -64,12 +64,12 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "40af578c-eac4-47ac-9c30-5596eceaf9df",
|
"id": "f93073a3-7753-4ce1-9ef1-2a0c16386543",
|
||||||
"name": "Power Manager - ensure-on",
|
"name": "Power Manager - ensure-on",
|
||||||
"type": "n8n-nodes-base.executeWorkflow",
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
"typeVersion": 1.3,
|
"typeVersion": 1.3,
|
||||||
"position": [
|
"position": [
|
||||||
2352,
|
1952,
|
||||||
1728
|
1728
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -78,12 +78,12 @@
|
||||||
"authentication": "privateKey",
|
"authentication": "privateKey",
|
||||||
"command": "=ssh vm101 'pi pending-raw {{ $('Gate push').first().json.genome }}'"
|
"command": "=ssh vm101 'pi pending-raw {{ $('Gate push').first().json.genome }}'"
|
||||||
},
|
},
|
||||||
"id": "f8861a50-aaf1-46fb-95a9-b9b200d4d6ae",
|
"id": "876dbdaf-3620-4c2c-a65b-336f0b11198c",
|
||||||
"name": "SSH: pending-raw",
|
"name": "SSH: pending-raw",
|
||||||
"type": "n8n-nodes-base.ssh",
|
"type": "n8n-nodes-base.ssh",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
2576,
|
2176,
|
||||||
1728
|
1728
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
|
|
@ -95,14 +95,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"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;"
|
"jsCode": "// Parse pending-raw -> one item per raw, carrying everything run-one-ingest needs.\n// Unsafe filenames (spaces / odd chars) are NOT ingested -> a 'badname' item -> ntfy.\nconst out = String($input.first().json.stdout || '').trim();\nlet d;\ntry {\n d = JSON.parse(out);\n} catch (e) {\n return [{ json: { _kind: 'error', reason: 'pending-raw non parsabile', raw: out.substring(0, 500) } }];\n}\n\nif (!d || typeof d !== 'object') {\n return [{ json: { _kind: 'error', reason: 'pending-raw non è un oggetto JSON', raw: out.substring(0, 500) } }];\n}\n\nconst files = Array.isArray(d.files) ? d.files : [];\nif (files.length === 0) return [];\n\n// Build reason map from detail array\nconst why = {};\nfor (const it of (Array.isArray(d.detail) ? d.detail : [])) {\n if (it && typeof it.path === 'string' && typeof it.reason === 'string') {\n why[it.path] = it.reason;\n }\n}\n\nconst SAFE = /^[A-Za-z0-9._\\/-]+$/;\nconst items = [];\nfor (const raw of files) {\n if (typeof raw !== 'string') {\n items.push({ json: { _kind: 'badname', genome: d.genome, raw: String(raw),\n hint: String(raw).replace(/[^A-Za-z0-9._\\/-]+/g, '-').toLowerCase() || 'invalid' } });\n continue;\n }\n if (SAFE.test(raw)) {\n items.push({ json: { _kind: 'ingest', genome: d.genome, raw,\n mode: 'ingest', feedback_b64: '', reason: why[raw] || 'new', prevPr: '' } });\n } else {\n const hint = raw.replace(/[^A-Za-z0-9._\\/-]+/g, '-').toLowerCase() || 'invalid';\n items.push({ json: { _kind: 'badname', genome: d.genome, raw, hint } });\n }\n}\nreturn items;"
|
||||||
},
|
},
|
||||||
"id": "e1f1e251-1565-4092-b8e7-b97c9c0bb18d",
|
"id": "f5bbbed3-222e-4129-a764-7cf47d69c5ce",
|
||||||
"name": "Split raw files",
|
"name": "Split raw files",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
2800,
|
2400,
|
||||||
1728
|
1728
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -130,12 +130,12 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "3339ce75-3ec9-4ed0-8faa-2433e9616c43",
|
"id": "5398e2c4-c7ca-4ca4-a2d7-e75077453b7c",
|
||||||
"name": "Nome valido?",
|
"name": "Nome valido?",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.if",
|
||||||
"typeVersion": 2.2,
|
"typeVersion": 2.2,
|
||||||
"position": [
|
"position": [
|
||||||
3024,
|
2624,
|
||||||
1728
|
1728
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -228,26 +228,26 @@
|
||||||
"waitForSubWorkflow": false
|
"waitForSubWorkflow": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "fda796f5-588b-4502-a653-5d27c3f72ac6",
|
"id": "0f274662-62bb-448b-ae4b-47e4bbcfd35a",
|
||||||
"name": "Run one ingest",
|
"name": "Run one ingest",
|
||||||
"type": "n8n-nodes-base.executeWorkflow",
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
"typeVersion": 1.3,
|
"typeVersion": 1.3,
|
||||||
"position": [
|
"position": [
|
||||||
3232,
|
2832,
|
||||||
1616
|
1616
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"mode": "runOnceForEachItem",
|
||||||
"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}\\`` };"
|
"jsCode": "// Build ntfy notification for files with invalid names.\n// Run Once for Each Item: $json is the current badname item.\nconst d = $json || {};\nconst genome = d.genome || 'unknown';\nconst raw = String(d.raw || 'unknown');\nconst hint = String(d.hint || 'unknown');\n\n// Escape backticks to avoid breaking markdown\nconst rawEsc = raw.replace(/`/g, '\\`');\nconst hintEsc = hint.replace(/`/g, '\\`');\n\nreturn {\n topic: 'genome-ingest',\n title: `${genome} · file da rinominare`,\n priority: 'high',\n tags: 'warning',\n click: '',\n actions: '',\n body: `Il file \\`${rawEsc}\\` ha spazi o caratteri non ammessi e **non** è stato ingerito.\\nRinominalo in: \\`${hintEsc}\\``\n};"
|
||||||
},
|
},
|
||||||
"id": "6820f3fb-97bb-45bf-8e7f-00eb68d7f313",
|
"id": "0f785bcd-cdc6-4dac-9ced-1c5cfa3453dc",
|
||||||
"name": "Build ntfy badname",
|
"name": "Build ntfy badname",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
3232,
|
2832,
|
||||||
1840
|
1840
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -294,12 +294,12 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "55cc42b2-3170-4884-bb5d-58e3af97bfea",
|
"id": "9cd2bde3-6846-4855-ad01-e3a4cdbce208",
|
||||||
"name": "ntfy: send",
|
"name": "ntfy: send",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
3456,
|
3056,
|
||||||
1840
|
1840
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
|
|
@ -410,7 +410,7 @@
|
||||||
"callerPolicy": "workflowsFromSameOwner",
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
"availableInMCP": false
|
"availableInMCP": false
|
||||||
},
|
},
|
||||||
"versionId": "d58601e7-b752-4c9f-9438-d91be663c82e",
|
"versionId": "63863925-606f-4200-824c-52f1919f2bb1",
|
||||||
"meta": {
|
"meta": {
|
||||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,27 @@
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"id": "eee467d7-5f8b-4abf-8923-1c70a29dafb2",
|
"id": "f715ed51-95e6-475f-8aa5-d0df531cc7cf",
|
||||||
"name": "Error Trigger",
|
"name": "Error Trigger",
|
||||||
"type": "n8n-nodes-base.errorTrigger",
|
"type": "n8n-nodes-base.errorTrigger",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
0,
|
688,
|
||||||
0
|
-32
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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}` };"
|
"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.\n// Run Once for Each Item: $json is the error trigger payload.\nconst e = $json.execution || {};\nconst w = $json.workflow || {};\n\n// Safely extract error message from various shapes\nconst rawMsg = (e.error && (e.error.message || e.error.description)) || 'errore sconosciuto';\nconst msg = String(rawMsg).trim();\n\nconst lastNode = e.lastNodeExecuted ? ` (nodo: ${e.lastNodeExecuted})` : '';\nconst workflowName = w.name || 'n8n';\nconst executionUrl = e.url || '';\n\n// Escape markdown to avoid breaking the notification body\nconst msgEsc = msg.replace(/`/g, '\\`').replace(/\\n/g, '\\n');\n\nreturn {\n topic: 'genome-ingest',\n title: `Workflow KO · ${workflowName}`,\n priority: 'high',\n tags: 'rotating_light',\n click: executionUrl,\n actions: executionUrl ? `view, Apri l'esecuzione, ${executionUrl}` : '',\n body: `**${workflowName}** è fallito${lastNode}.\\n\\n${msgEsc}`\n};"
|
||||||
},
|
},
|
||||||
"id": "bdbf5186-143d-4482-b873-5760fbdabab0",
|
"id": "dd39bc0f-918a-4645-8f04-540ac9089311",
|
||||||
"name": "Build ntfy",
|
"name": "Build ntfy",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
240,
|
928,
|
||||||
0
|
-32
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -69,13 +69,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "16e9a3af-6acc-46f2-bc56-79e185fddf53",
|
"id": "a9ee90f3-d7fe-445d-96af-12caef46473f",
|
||||||
"name": "ntfy: send",
|
"name": "ntfy: send",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
464,
|
1152,
|
||||||
0
|
-32
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
"executionOrder": "v1",
|
"executionOrder": "v1",
|
||||||
"binaryMode": "separate"
|
"binaryMode": "separate"
|
||||||
},
|
},
|
||||||
"versionId": "95bfb02a-7122-43d7-bec6-3a2e5b77a469",
|
"versionId": "036161c9-c934-474e-9b4f-634259f2a866",
|
||||||
"meta": {
|
"meta": {
|
||||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,27 @@
|
||||||
"path": "forgejo-push-prune",
|
"path": "forgejo-push-prune",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "050b318b-d3bb-4c1a-baa4-a1e2bd1babd8",
|
"id": "d31388b9-c6d6-4f28-9a6c-b381922bf5e0",
|
||||||
"name": "Webhook prune",
|
"name": "Webhook prune",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 2.1,
|
"typeVersion": 2.1,
|
||||||
"position": [
|
"position": [
|
||||||
0,
|
1232,
|
||||||
0
|
-64
|
||||||
],
|
],
|
||||||
"webhookId": "d6ac11900058434e"
|
"webhookId": "d6ac11900058434e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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 } }];"
|
"jsCode": "// Gate: proceed ONLY on develop pushes that REMOVED at least one file under raw/.\n// Additions/modifications are handled by the ingest flow; this flow reacts to deletions only.\nconst item = $input.first().json;\nconst b = item.body || item;\nconst ref = String(b.ref || '');\nconst genome = String((b.repository?.name) || '').toLowerCase().trim();\n\n// Branch filter\nif (ref !== 'refs/heads/develop') return [];\n\n// Genome name validation (DNS-like: lowercase alphanum + hyphen, 1-64 chars)\nif (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(genome)) return [];\n\n// Collect removed paths safely\nconst removed = [];\nfor (const c of (b.commits || [])) {\n if (!c || !Array.isArray(c.removed)) continue;\n for (const p of c.removed) {\n if (typeof p === 'string' && p.startsWith('raw/')) {\n removed.push(p);\n }\n }\n}\n\n// Gate: stop if nothing under raw/ was removed\nif (removed.length === 0) return [];\n\nreturn [{ json: { genome, removedCount: removed.length } }];"
|
||||||
},
|
},
|
||||||
"id": "deac740a-2046-4de8-91eb-812114edeb7b",
|
"id": "84848a31-d099-459e-bd03-67abc2cf2b77",
|
||||||
"name": "Gate prune",
|
"name": "Gate prune",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
224,
|
1456,
|
||||||
0
|
-64
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -64,13 +64,13 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "554737ea-09f3-4d4b-9a66-c983cc72e655",
|
"id": "175e4191-eb1b-4e5d-8d82-c39205753152",
|
||||||
"name": "Power Manager - ensure-on",
|
"name": "Power Manager - ensure-on",
|
||||||
"type": "n8n-nodes-base.executeWorkflow",
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
"typeVersion": 1.3,
|
"typeVersion": 1.3,
|
||||||
"position": [
|
"position": [
|
||||||
448,
|
1680,
|
||||||
0
|
-64
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -78,13 +78,13 @@
|
||||||
"authentication": "privateKey",
|
"authentication": "privateKey",
|
||||||
"command": "=ssh vm101 'pi orphan-wiki {{ $('Gate prune').first().json.genome }}'"
|
"command": "=ssh vm101 'pi orphan-wiki {{ $('Gate prune').first().json.genome }}'"
|
||||||
},
|
},
|
||||||
"id": "311005a4-16fd-4752-92d3-e3bbb9cdf19f",
|
"id": "598f20f8-d668-48da-90e3-1bfada3ace92",
|
||||||
"name": "SSH: orphan-wiki",
|
"name": "SSH: orphan-wiki",
|
||||||
"type": "n8n-nodes-base.ssh",
|
"type": "n8n-nodes-base.ssh",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
672,
|
1904,
|
||||||
0
|
-64
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"sshPrivateKey": {
|
"sshPrivateKey": {
|
||||||
|
|
@ -95,15 +95,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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 } }];"
|
"jsCode": "// Gate: proceed to prune only if orphan-wiki actually found orphans.\n// run-prune re-derives independently anyway (no detected-vs-pruned race);\n// this gate just avoids taking the lock for nothing.\nconst out = String($input.first().json.stdout || '').trim();\nlet d;\n\ntry {\n d = JSON.parse(out);\n} catch (e) {\n // Malformed JSON from orphan-wiki — log and stop\n return [{ json: { _gate: 'parse-error', raw: out.substring(0, 500) } }];\n}\n\n// Strict validation: d must be object with numeric count > 0\nif (!d || typeof d !== 'object' || typeof d.count !== 'number' || d.count <= 0) {\n return []; // 0 orphans or missing count -> stop silently\n}\n\nreturn [{ json: { genome: d.genome, count: d.count } }];"
|
||||||
},
|
},
|
||||||
"id": "2374b63c-f0db-4ae9-b350-3fec70687384",
|
"id": "3b644d61-26d8-4024-baed-bcb4ad169a6a",
|
||||||
"name": "Orfani?",
|
"name": "Orfani?",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
880,
|
2112,
|
||||||
0
|
-64
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -111,13 +111,13 @@
|
||||||
"authentication": "privateKey",
|
"authentication": "privateKey",
|
||||||
"command": "=ssh vm101 'pi prune {{ $json.genome }}'"
|
"command": "=ssh vm101 'pi prune {{ $json.genome }}'"
|
||||||
},
|
},
|
||||||
"id": "e4130173-ff62-4e11-b3d1-ee7870803663",
|
"id": "a8cae2c2-6f2f-4ef6-add9-287195aa84b5",
|
||||||
"name": "SSH: prune",
|
"name": "SSH: prune",
|
||||||
"type": "n8n-nodes-base.ssh",
|
"type": "n8n-nodes-base.ssh",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
1104,
|
2336,
|
||||||
0
|
-64
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"sshPrivateKey": {
|
"sshPrivateKey": {
|
||||||
|
|
@ -129,29 +129,29 @@
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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;"
|
"jsCode": "// Extract the last JSON line from SSH stdout (the command may print logs before/after).\n// Run Once for Each Item: $json is the current SSH result item.\nconst out = String($json.stdout || '').trim();\nconst jsonLines = out\n .split('\\n')\n .map(l => l.trim())\n .filter(l => l.startsWith('{') && l.endsWith('}'));\n\nconst line = jsonLines.pop(); // last JSON object line (command prints JSON last)\n\nlet r;\ntry {\n r = line ? JSON.parse(line) : { status: 'error', reason: 'nessuna riga JSON trovata in stdout' };\n} catch (e) {\n r = { status: 'error', reason: 'JSON non parsabile', rawLine: line?.substring(0, 1000) };\n}\n\n// Ensure consistent shape for downstream nodes\nreturn {\n status: r.status || 'error',\n reason: r.reason || 'errore sconosciuto',\n count: r.count,\n pr_url: r.pr_url,\n genome: r.genome,\n _raw: line?.substring(0, 500)\n};"
|
||||||
},
|
},
|
||||||
"id": "b649df9a-1e64-49cc-9e7f-6f78a1190382",
|
"id": "da1ab42c-32e1-4c4d-82a1-925fcee1a098",
|
||||||
"name": "Parse prune",
|
"name": "Parse prune",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1328,
|
2560,
|
||||||
0
|
-64
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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;"
|
"jsCode": "// Build ntfy notification for genome pruning.\n// Run Once for Each Item: $json is the parsed prune result.\nconst d = $json;\nconst genome = d.genome || 'unknown';\n\nlet n;\nif (d.status === 'ok') {\n const pm = (d.pr_url || '').match(/\\/pulls\\/(\\d+)/);\n const num = pm ? `#${pm[1]}` : '';\n n = {\n topic: 'genome-ingest',\n title: `${genome} \\u00b7 potatura ${num}`.replace(/\\s+/g, ' ').trim(),\n priority: 'default',\n tags: 'broom',\n click: d.pr_url || '',\n 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 };\n} else {\n n = {\n topic: 'genome-ingest',\n title: `${genome} \\u00b7 errore potatura`.trim(),\n priority: 'high',\n tags: 'rotating_light',\n click: '',\n actions: '',\n body: `${d.reason || 'errore sconosciuto durante la potatura'}.`\n };\n}\n\nreturn n;"
|
||||||
},
|
},
|
||||||
"id": "00be2a1f-6097-40e2-81f5-9dcba40b66ae",
|
"id": "ebe99407-6038-4f8f-a73f-7dc7b0a011e0",
|
||||||
"name": "Build ntfy",
|
"name": "Build ntfy",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1552,
|
2784,
|
||||||
0
|
-64
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -197,13 +197,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "50a36840-be50-43ec-bea9-44819a88a923",
|
"id": "0bd3654e-a73d-4c3a-83ed-9f57ca4aad24",
|
||||||
"name": "ntfy: send",
|
"name": "ntfy: send",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
1760,
|
2992,
|
||||||
0
|
-64
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -311,9 +311,13 @@
|
||||||
"active": true,
|
"active": true,
|
||||||
"settings": {
|
"settings": {
|
||||||
"executionOrder": "v1",
|
"executionOrder": "v1",
|
||||||
"binaryMode": "separate"
|
"binaryMode": "separate",
|
||||||
|
"timeSavedMode": "fixed",
|
||||||
|
"errorWorkflow": "7Vws3gCX3QnjM3oD",
|
||||||
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
|
"availableInMCP": false
|
||||||
},
|
},
|
||||||
"versionId": "ff0be89b-7930-4171-a547-5dc7bffc9472",
|
"versionId": "999f640c-aae6-42aa-9a95-aba26987e9d0",
|
||||||
"meta": {
|
"meta": {
|
||||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,27 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"inputSource": "passthrough"
|
"inputSource": "passthrough"
|
||||||
},
|
},
|
||||||
"id": "70da9144-1147-4cb5-9868-1f5ee2425d4c",
|
"id": "b1b7ba8e-1e45-4f76-adc0-089180715975",
|
||||||
"name": "On ingest request",
|
"name": "On ingest request",
|
||||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||||
"typeVersion": 1.1,
|
"typeVersion": 1.1,
|
||||||
"position": [
|
"position": [
|
||||||
-32,
|
224,
|
||||||
416
|
624
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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 || '' };"
|
"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.\n// Run Once for Each Item: $json is the current ingest request.\nconst d = $json || {};\nconst genome = String(d.genome || '').toLowerCase().trim();\nconst raw = String(d.raw || '');\nconst mode = String(d.mode || 'ingest');\nconst fb = String(d.feedback_b64 || '');\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);\n// feedback_b64 is required only for rework mode; for ingest it can be empty\nconst okFb = (mode === 'ingest') || /^[A-Za-z0-9+/=]+$/.test(fb);\n\nif (!okGenome || !okMode || !okRaw || !okFb) {\n return {\n _ok: false,\n genome,\n mode,\n _reason: `bad input (genome:${okGenome} mode:${okMode} raw:${okRaw} fb:${okFb})`\n };\n}\n\n// Build SSH command: single-quoted remote command prevents shell injection\nconst ssh_cmd = (mode === 'rework')\n ? `ssh vm101 'pi ingest-rework ${genome} ${raw} ${fb}'`\n : `ssh vm101 'pi ingest ${genome} ${raw}'`;\n\nreturn {\n _ok: true,\n ssh_cmd,\n genome,\n raw,\n mode,\n reason: String(d.reason || ''),\n prevPr: String(d.prevPr || '')\n};"
|
||||||
},
|
},
|
||||||
"id": "551ec0f1-450c-41ce-88a1-8690bc2c1c0b",
|
"id": "8e538237-0e0e-4308-b2c8-631a52b31185",
|
||||||
"name": "Guard & build cmd",
|
"name": "Guard & build cmd",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
192,
|
448,
|
||||||
416
|
624
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -52,13 +52,13 @@
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"id": "ea4c36ed-c452-406b-9c94-c58fdc69ed20",
|
"id": "4b249e76-7ab6-4aa3-886d-06b865931cf6",
|
||||||
"name": "Input valido?",
|
"name": "Input valido?",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.if",
|
||||||
"typeVersion": 2.2,
|
"typeVersion": 2.2,
|
||||||
"position": [
|
"position": [
|
||||||
416,
|
672,
|
||||||
416
|
624
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -66,13 +66,13 @@
|
||||||
"authentication": "privateKey",
|
"authentication": "privateKey",
|
||||||
"command": "={{ $json.ssh_cmd }}"
|
"command": "={{ $json.ssh_cmd }}"
|
||||||
},
|
},
|
||||||
"id": "a5ea3f08-df3b-4433-a04e-b69ce742575f",
|
"id": "8740ae9a-4094-48b2-a9a4-d40d501e09f6",
|
||||||
"name": "SSH: ingest",
|
"name": "SSH: ingest",
|
||||||
"type": "n8n-nodes-base.ssh",
|
"type": "n8n-nodes-base.ssh",
|
||||||
"typeVersion": 1,
|
"typeVersion": 1,
|
||||||
"position": [
|
"position": [
|
||||||
624,
|
880,
|
||||||
336
|
544
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"sshPrivateKey": {
|
"sshPrivateKey": {
|
||||||
|
|
@ -84,29 +84,29 @@
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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;"
|
"jsCode": "// run-ingest.sh prints one JSON line; the wrapper may instead print {status:busy|error,...}.\n// Take the last {...} line from stdout (logs may precede/follow).\n// Run Once for Each Item: $json is the current SSH result item.\nconst out = String($json.stdout || '').trim();\nconst jsonLines = out\n .split('\\n')\n .map(l => l.trim())\n .filter(l => l.startsWith('{') && l.endsWith('}'));\n\nconst line = jsonLines.pop(); // last JSON object line (command prints JSON last)\n\nlet r;\ntry {\n r = line ? JSON.parse(line) : { status: 'error', reason: 'nessuna riga JSON trovata in stdout', raw: out.substring(0, 500) };\n} catch (e) {\n r = { status: 'error', reason: 'JSON non parsabile', rawLine: line?.substring(0, 1000) };\n}\n\n// Ensure consistent shape for downstream Build ntfy\nreturn {\n status: r.status || 'error',\n reason: r.reason || 'errore sconosciuto',\n pr_url: r.pr_url || '',\n slug: r.slug || '',\n lint_clean: r.lint_clean || false,\n conflict: r.conflict || false,\n stage: r.stage || '',\n detail: r.detail || '',\n log: r.log || '',\n _raw: line?.substring(0, 500)\n};"
|
||||||
},
|
},
|
||||||
"id": "0ee5e6c2-111a-4458-aab7-20a683f027ee",
|
"id": "928344e3-0712-42e0-b1a8-f5caff489746",
|
||||||
"name": "Parse result",
|
"name": "Parse result",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
848,
|
1104,
|
||||||
336
|
544
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForEachItem",
|
"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;"
|
"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.\n// Run Once for Each Item: $json is the current parsed result.\n// We read the original request context from the Guard node (same execution, no executeWorkflow in between).\nconst g = $('Guard & build cmd').item.json || {};\nconst verb = (g.mode === 'rework') ? 'rework' : 'ingest';\nconst d = $json || {};\nconst genome = g.genome || 'unknown';\n\n// Build notification based on status\nlet n;\n\nif (g._ok === false) {\n // Input validation failed (Guard & build cmd rejected it)\n n = {\n title: `Errore ${verb}: input non valido`,\n priority: 'high',\n tags: 'rotating_light',\n click: '',\n actions: '',\n body: `Richiesta di ${verb} rifiutata.\\n${g._reason || 'motivo sconosciuto'}`\n };\n} else if (d.status === 'ok') {\n // Success: PR opened\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 ? ' · ⚠️ conflitto da risolvere' : '';\n const prevPr = g.prevPr ? ` · sostituisce #${g.prevPr}` : '';\n const reason = (g.reason && verb === 'ingest') ? ` (${g.reason})` : '';\n\n n = {\n title: `${genome} · ${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 || '',\n actions: d.pr_url ? `view, Apri la PR, ${d.pr_url}` : '',\n body: `**${d.slug || 'sorgente'}** ${verb === 'rework' ? 'rilavorata' : 'ingerita'}`\n + reason + prevPr\n + `.\\n${lint}${conflict}.`\n };\n} else if (d.status === 'busy') {\n // Another ingest is already running on this genome\n n = {\n title: `${genome} · ${verb} in coda`,\n priority: 'min',\n tags: 'hourglass_flowing_sand',\n click: '',\n actions: '',\n body: `Un altro ingest era in corso su questo genoma. La fonte resta pendente e verrà ripresa al prossimo campanello.`\n };\n} else if (d.status === 'pr_failed') {\n // Semantic/lint ok but PR could not be opened\n const detailLine = String(d.detail || '').split('\\n')[0] || 'dettaglio non disponibile';\n n = {\n title: `${genome} · ${d.slug || ''}: PR non aperta`,\n priority: 'high',\n tags: 'warning',\n click: '',\n actions: '',\n body: `Semantic e lint ok, ma la PR non si è aperta.\\n${detailLine}`\n };\n} else {\n // Generic error (including parse errors)\n const stage = d.stage ? ` (stage: ${d.stage})` : '';\n const log = d.log ? `\\nLog: ${d.log}` : '';\n n = {\n title: `${genome} · errore ${verb}`,\n priority: 'high',\n tags: 'rotating_light',\n click: '',\n actions: '',\n body: `${d.reason || 'errore sconosciuto'}${stage}.${log}`\n };\n}\n\nn.topic = 'genome-ingest';\nreturn n;"
|
||||||
},
|
},
|
||||||
"id": "458318e5-7b26-4695-b564-f58c357d37d0",
|
"id": "9062dfba-02ba-4abc-8be6-828c0b353114",
|
||||||
"name": "Build ntfy",
|
"name": "Build ntfy",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [
|
"position": [
|
||||||
1072,
|
1328,
|
||||||
416
|
624
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -152,13 +152,13 @@
|
||||||
"timeout": 15000
|
"timeout": 15000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"id": "4a1f0a89-1a56-4e1c-8fbc-173cba4ce97b",
|
"id": "0c2b4d9b-2700-4815-b47c-8523bc4eb2ff",
|
||||||
"name": "ntfy: send",
|
"name": "ntfy: send",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.4,
|
"typeVersion": 4.4,
|
||||||
"position": [
|
"position": [
|
||||||
1296,
|
1552,
|
||||||
416
|
624
|
||||||
],
|
],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"httpHeaderAuth": {
|
"httpHeaderAuth": {
|
||||||
|
|
@ -257,7 +257,7 @@
|
||||||
"callerPolicy": "workflowsFromSameOwner",
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
"availableInMCP": false
|
"availableInMCP": false
|
||||||
},
|
},
|
||||||
"versionId": "5d2cf4bd-f2c6-41fc-98a5-eaa797e31417",
|
"versionId": "fd8c1cf6-c5df-4074-b777-113349e32a03",
|
||||||
"meta": {
|
"meta": {
|
||||||
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
"instanceId": "96b2f0ec76a4400bbd481c617b24b3b87024cc7a913efacccaf9fc85722e7417"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue