{ "name": "Docker Event (Extended)", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "docker-event", "responseMode": "responseNode", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [0, 0], "id": "webhook-main", "name": "Webhook" }, { "parameters": { "jsCode": "const data = $input.first().json;\n\nconst container = data.container ?? data.body?.container ?? '';\nconst image = data.image ?? data.body?.image ?? '';\nconst timestamp = data.timestamp ?? data.body?.timestamp ?? '';\n\nconst slug = container.toLowerCase().replace(/[^a-z0-9]+/g, '-');\nconst serviceName = container.replace(/[-_]/g, ' ');\n\n// Detect project type\nlet projectType = 'selfhosted';\nif (image.includes('denshooter') || image.includes('dk0')) {\n projectType = 'own';\n} else if (container.match(/^(act-|gitea-actions-|runner-)/)) {\n projectType = 'cicd';\n}\n\n// Extract repo from image for own projects\nlet repo = null;\nif (projectType === 'own') {\n const match = image.match(/([^/]+):(\\w+)/);\n if (match) repo = match[1];\n}\n\nreturn [{\n json: {\n container,\n image,\n serviceName,\n timestamp,\n slug,\n projectType,\n repo\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [220, 0], "id": "parse-context", "name": "Parse Context" }, { "parameters": { "url": "=https://cms.dk0.dev/items/projects?filter[slug][_eq]={{ $json.slug }}&limit=1", "authentication": "predefinedCredentialType", "nodeCredentialType": "httpBearerAuth", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [440, 0], "id": "search-slug", "name": "Check if Exists" }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "leftValue": "={{ $json.data.length }}", "rightValue": "0", "operator": { "type": "number", "operation": "equals" } } ], "combinator": "and" }, "options": {} }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [660, 0], "id": "if-new", "name": "If New" }, { "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "" }, "conditions": [ { "leftValue": "={{ $('Parse Context').item.json.projectType }}", "rightValue": "own", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Own Project" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "" }, "conditions": [ { "leftValue": "={{ $('Parse Context').item.json.projectType }}", "rightValue": "cicd", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "CI/CD (Ignore)" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "" }, "conditions": [ { "leftValue": "={{ $('Parse Context').item.json.projectType }}", "rightValue": "selfhosted", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Self-Hosted" } ] }, "options": {} }, "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [880, 0], "id": "switch-type", "name": "Switch Type" }, { "parameters": { "url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $('Parse Context').item.json.repo }}/commits?limit=1", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [1100, -200], "id": "get-commits", "name": "Get Last Commit", "credentials": { "httpHeaderAuth": { "id": "gitea-token", "name": "Gitea API" } } }, { "parameters": { "url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $('Parse Context').item.json.repo }}/contents/README.md", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [1100, -80], "id": "get-readme", "name": "Get README" }, { "parameters": { "jsCode": "const ctx = $('Parse Context').first().json;\nconst commits = $('Get Last Commit').first().json;\nconst readme = $('Get README').first().json;\n\n// Get commit data\nconst commit = Array.isArray(commits) ? commits[0] : commits;\nconst commitMsg = commit?.commit?.message || 'No recent commits';\nconst commitAuthor = commit?.commit?.author?.name || 'Unknown';\n\n// Decode README (base64)\nlet readmeText = '';\ntry {\n const content = readme?.content || readme?.data?.content;\n if (content) {\n readmeText = Buffer.from(content, 'base64').toString('utf8');\n // First 500 chars\n readmeText = readmeText.substring(0, 500).replace(/\\n/g, ' ').trim();\n } else {\n readmeText = 'No README available';\n }\n} catch (e) {\n readmeText = 'No README available';\n}\n\nconsole.log('Commit:', commitMsg);\nconsole.log('README excerpt:', readmeText.substring(0, 100));\n\nreturn [{\n json: {\n container: ctx.container,\n image: ctx.image,\n slug: ctx.slug,\n repo: ctx.repo,\n commitMsg,\n commitAuthor,\n readmeExcerpt: readmeText\n }\n}];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1320, -140], "id": "merge-git-data", "name": "Merge Git Data" }, { "parameters": { "chatId": "145931600", "text": "={{ \n'🚀 Neuer Deploy: ' + $json.container + '\\n' +\n'📦 ' + $json.image + '\\n\\n' +\n'📝 Letzter Commit:\\n' + $json.commitMsg + '\\n' +\n'👤 ' + $json.commitAuthor + '\\n\\n' +\n'📄 README:\\n' + $json.readmeExcerpt + '...\\n\\n' +\n'Was ist das Highlight?' \n}}", "additionalFields": { "replyMarkup": "inlineKeyboard", "inlineKeyboard": { "rows": [ { "buttons": [ { "text": "✍️ Selbst beschreiben", "callbackData": "={{ 'manual:' + $json.slug }}" }, { "text": "🤖 Auto-generieren", "callbackData": "={{ 'auto:' + $json.slug }}" } ] }, { "buttons": [ { "text": "❌ Ignorieren", "callbackData": "={{ 'ignore:' + $json.slug }}" } ] } ] } } }, "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [1540, -140], "id": "telegram-ask", "name": "Ask via Telegram" }, { "parameters": { "model": "openrouter/free", "options": {} }, "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter", "typeVersion": 1, "position": [1540, 160], "id": "openrouter-model", "name": "OpenRouter Chat Model" }, { "parameters": { "promptType": "define", "text": "=Du bist ein technischer Autor für dk0.dev.\n\nNeuer Self-Hosted Service:\nContainer: {{ $('Parse Context').item.json.container }}\nImage: {{ $('Parse Context').item.json.image }}\n\nErstelle eine Portfolio-Beschreibung:\n- Was macht die App\n- Warum Self-Hosting besser ist als Cloud\n- Wie sie in die Infrastruktur integriert ist\n\nAntworte NUR als JSON:\n{\n \"title_en\": \"Titel\",\n \"title_de\": \"Titel\",\n \"description_en\": \"4-6 Sätze\",\n \"description_de\": \"4-6 Sätze\",\n \"content_en\": \"2-3 Absätze Markdown\",\n \"content_de\": \"2-3 Absätze Markdown\",\n \"category\": \"selfhosted\",\n \"technologies\": [\"Docker\", \"...\"]\n}" }, "type": "@n8n/n8n-nodes-langchain.chainLlm", "typeVersion": 1.9, "position": [1320, 80], "id": "ai-selfhosted", "name": "AI: Self-Hosted" }, { "parameters": { "jsCode": "const raw = $input.first().json.text ?? \"\";\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\nconst ai = JSON.parse(match[0]);\nreturn [{ json: ai }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1540, 80], "id": "parse-json-selfhosted", "name": "Parse JSON" }, { "parameters": { "jsCode": "const ai = $input.first().json;\nconst ctx = $('Parse Context').first().json;\n\nconst body = {\n slug: ctx.slug,\n status: \"draft\",\n featured: false,\n title: ai.title_en,\n category: ai.category,\n technologies: ai.technologies,\n tags: ai.technologies,\n date: new Date().toISOString().slice(0, 10),\n translations: {\n create: [\n {\n languages_code: \"en-US\",\n title: ai.title_en,\n description: ai.description_en,\n content: ai.content_en\n },\n {\n languages_code: \"de-DE\",\n title: ai.title_de,\n description: ai.description_de,\n content: ai.content_de\n }\n ]\n }\n};\n\nconst response = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://cms.dk0.dev/items/projects\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\"\n },\n body\n});\n\nreturn [{ json: response }];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1760, 80], "id": "add-to-directus-selfhosted", "name": "Add to Directus" }, { "parameters": { "chatId": "145931600", "text": "={{ \n'🆕 Self-Hosted Service: ' + $('Parse Context').first().json.serviceName + '\\n\\n' +\n'📝 ' + $json.data.title + '\\n\\n' +\n'Status: Draft erstellt (ID: ' + $json.data.id + ')\\n\\n' +\n'/publishproject' + $json.data.id + ' — Veröffentlichen\\n' + \n'/deleteproject' + $json.data.id + ' — Löschen' \n}}", "additionalFields": {} }, "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [1980, 80], "id": "telegram-notify-selfhosted", "name": "Notify Selfhosted" }, { "parameters": { "respondWith": "json", "responseBody": "{ \"success\": true, \"message\": \"CI/CD container ignored\" }", "options": {} }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [1100, 200], "id": "respond-ignore", "name": "Respond (Ignore)" }, { "parameters": { "respondWith": "json", "responseBody": "{ \"success\": true }", "options": {} }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [2200, 0], "id": "respond-success", "name": "Respond" }, { "parameters": { "respondWith": "json", "responseBody": "{ \"success\": true, \"message\": \"Project already exists\" }", "options": {} }, "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.5, "position": [880, 200], "id": "respond-exists", "name": "Respond (Exists)" } ], "connections": { "Webhook": { "main": [[{ "node": "Parse Context", "type": "main", "index": 0 }]] }, "Parse Context": { "main": [[{ "node": "Check if Exists", "type": "main", "index": 0 }]] }, "Check if Exists": { "main": [[{ "node": "If New", "type": "main", "index": 0 }]] }, "If New": { "main": [ [{ "node": "Switch Type", "type": "main", "index": 0 }], [{ "node": "Respond (Exists)", "type": "main", "index": 0 }] ] }, "Switch Type": { "main": [ [{ "node": "Get Last Commit", "type": "main", "index": 0 }], [{ "node": "Respond (Ignore)", "type": "main", "index": 0 }], [{ "node": "AI: Self-Hosted", "type": "main", "index": 0 }] ] }, "Get Last Commit": { "main": [[{ "node": "Get README", "type": "main", "index": 0 }]] }, "Get README": { "main": [[{ "node": "Merge Git Data", "type": "main", "index": 0 }]] }, "Merge Git Data": { "main": [[{ "node": "Ask via Telegram", "type": "main", "index": 0 }]] }, "Ask via Telegram": { "main": [[{ "node": "Respond", "type": "main", "index": 0 }]] }, "OpenRouter Chat Model": { "ai_languageModel": [[{ "node": "AI: Self-Hosted", "type": "ai_languageModel", "index": 0 }]] }, "AI: Self-Hosted": { "main": [[{ "node": "Parse JSON", "type": "main", "index": 0 }]] }, "Parse JSON": { "main": [[{ "node": "Add to Directus", "type": "main", "index": 0 }]] }, "Add to Directus": { "main": [[{ "node": "Notify Selfhosted", "type": "main", "index": 0 }]] }, "Notify Selfhosted": { "main": [[{ "node": "Respond", "type": "main", "index": 0 }]] } }, "active": false, "settings": { "executionOrder": "v1" }, "id": "docker-event-extended" }