{"id":"Ef2uEM6H19K2DGUO","meta":{"instanceId":"workflow-02e2d419","versionId":"1.0.0","createdAt":"2025-09-29T07:07:43.328879","updatedAt":"2025-09-29T07:07:43.328889","owner":"n8n-user","license":"MIT","category":"automation","status":"active","priority":"high","environment":"production"},"name":"Backup workflows to git repository on Gitea","nodes":[{"id":"639582ef-f13e-4844-bd10-647718079121","name":"Globals","type":"n8n-nodes-base.set","position":[600,240],"parameters":{"values":{"string":[{"name":"repo.url","value":"{{ $env.WEBHOOK_URL }}"},{"name":"repo.name","value":"workflows"},{"name":"repo.owner","value":"n8n"}]},"options":{}},"typeVersion":1,"notes":"This set node performs automated tasks as part of the workflow."},{"id":"9df89713-220e-43b9-b234-b8f5612629cf","name":"n8n","type":"n8n-nodes-base.n8n","position":[840,240],"parameters":{"filters":{},"requestOptions":{}},"credentials":{"n8nApi":{"id":"ZjfxOLTTHX2CzbKa","name":"Main N8N Account"}},"typeVersion":1,"notes":"This n8n node performs automated tasks as part of the workflow."},{"id":"4b2d375c-a339-404c-babd-555bd2fc4091","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","position":[380,240],"parameters":{"rule":{"interval":[{"field":"minutes","minutesInterval":45}]}},"typeVersion":1.2,"notes":"This scheduleTrigger node performs automated tasks as part of the workflow."},{"id":"ea026e96-0db1-41fd-b003-2f2bf4662696","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[2620,300],"parameters":{"height":80,"content":"Workflow changes committed to the repository"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"9c402daa-6d03-485d-b8a0-58f1b65d396d","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[2260,180],"parameters":{"height":80,"content":"Check if there are any changes in the workflow"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"1d9216d9-bf8d-4945-8a58-22fb1ffc9be8","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[1800,580],"parameters":{"height":80,"content":"Create a new file for the workflow"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"60a3953b-d9f1-4afd-b299-e314116b96c6","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[1300,200],"parameters":{"height":80,"content":"Check if file exists in the repository"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"f2340ad0-71a1-4c74-8d90-bcb974b8b305","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[780,180],"parameters":{"height":80,"content":"Get all workflows"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"617bea19-341a-4e9d-b6fd-6b417e58d756","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[500,180],"parameters":{"height":80,"content":"Set variables"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."},{"id":"72f806d7-e30a-470b-9ba2-37fdc35de3c8","name":"SetDataUpdateNode","type":"n8n-nodes-base.set","position":[1920,240],"parameters":{"options":{},"assignments":{"assignments":[{"id":"0a6b769a-c66d-4784-92c7-a70caa28e1ba","name":"item","type":"object","value":"={{ $node[\"ForEach\"].json }}"}]}},"typeVersion":3.4,"notes":"This set node performs automated tasks as part of the workflow."},{"id":"bca5e2c4-7aa3-48df-9e5f-b31977970c28","name":"SetDataCreateNode","type":"n8n-nodes-base.set","position":[1220,640],"parameters":{"options":{},"assignments":{"assignments":[{"id":"0a6b769a-c66d-4784-92c7-a70caa28e1ba","name":"item","type":"object","value":"={{ $node[\"ForEach\"].json }}"}]}},"typeVersion":3.4,"notes":"This set node performs automated tasks as part of the workflow."},{"id":"bf74b1ea-e066-462b-9c3d-ed4a44a09a33","name":"Base64EncodeUpdate","type":"n8n-nodes-base.code","position":[2140,240],"parameters":{"language":"python","pythonCode":"import json\nimport base64\nfrom js import Object\n\n# Assuming _input.all() returns a JavaScript object\njs_object = _input.all()\n\n# Convert the JsProxy object to a Python dictionary\ndef js_to_py(js_obj):\n    if isinstance(js_obj, (str, int, float, bool)) or js_obj is None:\n        # Base types are already Python-compatible\n        return js_obj\n    elif isinstance(js_obj, list):\n        # Convert lists recursively\n        return [js_to_py(item) for item in js_obj]\n    elif hasattr(js_obj, \"__iter__\") and not isinstance(js_obj, str):\n        # Handle JsProxy objects (JavaScript objects or arrays)\n        if hasattr(js_obj, \"keys\"):\n            # If it has keys, treat it as a dictionary\n            return {key: js_to_py(js_obj[key]) for key in Object.keys(js_obj)}\n        else:\n            # Otherwise, treat it as a list\n            return [js_to_py(item) for item in js_obj]\n    else:\n        # Fallback for other types\n        return js_obj\n\n# Convert the JavaScript object to a Python dictionary\ninput_dict = js_to_py(js_object)\n\n# Step 0: get the correct data set of the workflow\ninner_data = input_dict[0].get('json').get('item')\n\n# Step 1: Convert the dictionary to a pretty-printed JSON string\njson_string = json.dumps(inner_data, indent=4)\n\n# Step 2: Encode the JSON string to bytes\njson_bytes = json_string.encode('utf-8')\n\n# Step 3: Convert the bytes to a base64 string\nbase64_string = base64.b64encode(json_bytes).decode('utf-8')\n\n# Step 5: Create the return object with the base64 string and its SHA-256 hash\nreturn_object = {\n    \"item\": base64_string\n}\n\n# Return the object\nreturn return_object"},"typeVersion":2,"notes":"This code node performs automated tasks as part of the workflow."},{"id":"2d817c66-5aa0-45c9-b851-4b5e3dbecca4","name":"Base64EncodeCreate","type":"n8n-nodes-base.code","position":[1520,640],"parameters":{"language":"python","pythonCode":"import json\nimport base64\nfrom js import Object\n\n# Assuming _input.all() returns a JavaScript object\njs_object = _input.all()\n\n# Convert the JsProxy object to a Python dictionary\ndef js_to_py(js_obj):\n    if isinstance(js_obj, (str, int, float, bool)) or js_obj is None:\n        # Base types are already Python-compatible\n        return js_obj\n    elif isinstance(js_obj, list):\n        # Convert lists recursively\n        return [js_to_py(item) for item in js_obj]\n    elif hasattr(js_obj, \"__iter__\") and not isinstance(js_obj, str):\n        # Handle JsProxy objects (JavaScript objects or arrays)\n        if hasattr(js_obj, \"keys\"):\n            # If it has keys, treat it as a dictionary\n            return {key: js_to_py(js_obj[key]) for key in Object.keys(js_obj)}\n        else:\n            # Otherwise, treat it as a list\n            return [js_to_py(item) for item in js_obj]\n    else:\n        # Fallback for other types\n        return js_obj\n\n# Convert the JavaScript object to a Python dictionary\ninput_dict = js_to_py(js_object)\n\n# Step 0: get the correct data set of the workflow\ninner_data = input_dict[0].get('json').get('item')\n\n# Step 1: Convert the dictionary to a pretty-printed JSON string\njson_string = json.dumps(inner_data, indent=4)\n\n# Step 2: Encode the JSON string to bytes\njson_bytes = json_string.encode('utf-8')\n\n# Step 3: Convert the bytes to a base64 string\nbase64_string = base64.b64encode(json_bytes).decode('utf-8')\n\n# Step 4: Create the return object with the base64 string in 'item'\nreturn_object = {\n    \"item\": base64_string\n}\n\n# Return the object\nreturn return_object"},"typeVersion":2,"notes":"This code node performs automated tasks as part of the workflow."},{"id":"41a7da89-1c8c-4100-8c30-d0788962efc1","name":"Exist","type":"n8n-nodes-base.if","position":[1640,260],"parameters":{"options":{"ignoreCase":false},"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"or","conditions":[{"id":"16a9182d-059d-4774-ba95-654fb4293fdb","operator":{"type":"object","operation":"notExists","singleValue":true},"leftValue":"={{ $json.error }}","rightValue":404}]}},"executeOnce":false,"typeVersion":2.2,"alwaysOutputData":false,"notes":"This if node performs automated tasks as part of the workflow."},{"id":"ab9246eb-a253-4d76-b33b-5f8f12342542","name":"Changed","type":"n8n-nodes-base.if","position":[2360,240],"parameters":{"options":{},"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"e0c66624-429a-4f1f-bf7b-1cc1b32bad7b","operator":{"type":"string","operation":"notEquals"},"leftValue":"={{ $json.item }}","rightValue":"={{ $('GetGitea').item.json.content }}"}]}},"typeVersion":2.2,"notes":"This if node performs automated tasks as part of the workflow."},{"id":"4278a176-6496-4817-82f8-591539619673","name":"PutGitea","type":"n8n-nodes-base.httpRequest","position":[2700,360],"parameters":{"url":"{{ $env.BASE_URL }}","method":"PUT","options":{},"sendBody":true,"authentication":"{{ $credentials.genericCredentialType }}","bodyParameters":{"parameters":[{"name":"content","value":"={{ $('Base64EncodeUpdate').item.json.item }}"},{"name":"sha","value":"={{ $('GetGitea').item.json.sha }}"}]},"genericAuthType":"httpHeaderAuth"},"credentials":{"httpHeaderAuth":{"id":"gTvBAgkOmqhl5Nmr","name":"Gitea Token"}},"typeVersion":4.2,"notes":"This httpRequest node performs automated tasks as part of the workflow."},{"id":"12307a61-e7cc-42f9-a7c7-8abbcab9e3ab","name":"GetGitea","type":"n8n-nodes-base.httpRequest","onError":"continueRegularOutput","position":[1380,260],"parameters":{"url":"{{ $env.BASE_URL }}","options":{},"authentication":"{{ $credentials.genericCredentialType }}","genericAuthType":"httpHeaderAuth"},"credentials":{"httpHeaderAuth":{"id":"gTvBAgkOmqhl5Nmr","name":"Gitea Token"}},"typeVersion":4.2,"notes":"This httpRequest node performs automated tasks as part of the workflow."},{"id":"24fda439-bb23-4392-a297-d8070907f9e6","name":"PostGitea","type":"n8n-nodes-base.httpRequest","position":[1920,640],"parameters":{"url":"{{ $env.BASE_URL }}","method":"POST","options":{},"sendBody":true,"authentication":"{{ $credentials.genericCredentialType }}","bodyParameters":{"parameters":[{"name":"content","value":"={{ $json.item }}"}]},"genericAuthType":"httpHeaderAuth"},"credentials":{"httpHeaderAuth":{"id":"gTvBAgkOmqhl5Nmr","name":"Gitea Token"}},"typeVersion":4.2,"notes":"This httpRequest node performs automated tasks as part of the workflow."},{"id":"43a60315-d381-4ac4-be4c-f6a158651a00","name":"ForEach","type":"n8n-nodes-base.splitInBatches","position":[1060,240],"parameters":{"options":{}},"executeOnce":false,"typeVersion":3,"notes":"This splitInBatches node performs automated tasks as part of the workflow."},{"id":"88578dc4-2398-48d0-b0ba-2198b35bb994","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[380,440],"parameters":{"width":560,"height":1620,"content":"### **📌 Setup Guide for Backup Workflows to Git Repository on Gitea**\n\n#### **🔧 1. Configure Global Variables**\nGo to the **Globals** node and update the following:\n- **`repo.url`** → `{{ $env.WEBHOOK_URL }}` *(Replace with your actual Gitea URL)*\n- **`repo.name`** → `workflows` *(Repository name where backups will be stored)*\n- **`repo.owner`** → `octoleo` *(Gitea account that owns the repository)*\n\n📌 **These settings define where workflows will be backed up.**\n\n---\n\n#### **🔑 2. Set Up Gitea Authentication**\n1️⃣ **In Gitea:**\n- Generate a **Personal Access Token** under **Settings → Applications → Generate Token**\n- Ensure the token has **repo read/write permissions**\n\n2️⃣ **In the Credentials Manager:**\n- Create a new **Gitea Token** credential\n- Set the **Name** as `Authorization`\n- Set the **Value** as:\n```\nBearer YOUR_PERSONAL_ACCESS_TOKEN\n```\n📌 **Ensure there is a space after `Bearer` before the token!**\n\n---\n\n#### **🔗 3. Connect Gitea Credentials to Git Nodes**\n- Open each of these **three Git nodes**:\n- **GetGitea** → Retrieves existing repository data\n- **PutGitea** → Updates workflows\n- **PostGitea** → Adds new workflows\n\n- Assign the **Gitea Token** credential to each node.\n\n📌 **These nodes handle pushing your workflows to Gitea.**\n\n---\n\n#### **🌐 4. Set Up API Credentials for Workflow Retrieval**\n- Locate the API request node that **fetches workflows**.\n- Add your **API authentication credentials** (Token or Basic Auth).\n\n📌 **This ensures the workflow can fetch all available workflows from your system.**\n\n---\n\n#### **🛠️ 5. Test & Activate the Workflow**\n✅ **Run the workflow manually** → Check that workflows are being backed up correctly.\n✅ **Review the Gitea repository** → Ensure the files are updated.\n✅ **Enable the scheduled trigger** → Automates backups at defined intervals.\n\n📌 **The workflow automatically checks for changes before committing updates!**\n\n---\n\n### **🚀 Done! Your Workflows Are Now Backed Up Securely!**\n💬 Have issues? **Reach out on the forum for help!**"},"typeVersion":1,"notes":"This stickyNote node performs automated tasks as part of the workflow."}],"active":false,"settings":{"executionOrder":"v1","saveManualExecutions":true,"callerPolicy":"workflowsFromSameOwner","errorWorkflow":null,"timezone":"UTC","executionTimeout":3600,"maxExecutions":1000,"retryOnFail":true,"retryCount":3,"retryDelay":1000},"versionId":"84ba3f3f-fbc8-4792-8e28-198f515fef4e","staticData":{"node:Schedule Trigger":{"recurrenceRules":[]}},"connections":{"PutGitea":{"main":[[],[],[],[],[],[],[],[],[]]},"GetGitea":{"main":[[],[],[],[],[],[],[],[],[]]},"PostGitea":{"main":[[],[],[],[],[],[],[],[],[]]}},"triggerCount":1,"description":"Automated workflow: Backup workflows to git repository on Gitea. This workflow integrates 9 different services: stickyNote, httpRequest, code, scheduleTrigger, n8n. It contains 26 nodes and follows best practices for error handling and security.","notes":"Excellent quality workflow: Backup workflows to git repository on Gitea. This workflow has been optimized for production use with comprehensive error handling, security, and documentation."}