AutomationFlowsEmail & Gmail › Sync .env to Xcode via GitHub

Sync .env to Xcode via GitHub

Original n8n title: Ios Environment Config Sync Wizard: .env to Xcode

iOS Environment Config Sync Wizard: .env to Xcode. Uses gmail, httpRequest, githubTrigger. Event-driven trigger; 12 nodes.

Event trigger★★★★☆ complexity12 nodesGmailHTTP RequestGithub Trigger
Email & Gmail Trigger: Event Nodes: 12 Complexity: ★★★★☆ Added:

This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "zNx6ArX0ZtTTcfqp",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "iOS Environment Config Sync Wizard: .env to Xcode",
  "tags": [],
  "nodes": [
    {
      "id": "252bad23-e84c-4e8b-bb4d-81aba9315782",
      "name": "Check Changed Files",
      "type": "n8n-nodes-base.code",
      "position": [
        420,
        280
      ],
      "parameters": {
        "jsCode": "const webhookData = $input.first().json.body;\n\n// Log the entire webhookData for debugging\nconsole.log(webhookData);\n\n//const envFilePath = $node[\"SetConfiguration\"].json[\"envFilePath\"];\n\nconst envFilePath = \n$input.first().json.envFilePath\n// Ensure commits array exists and is not empty\nconst commits = webhookData.commits || [];\nconst changedFiles = commits.flatMap(commit => commit.added.concat(commit.modified, commit.removed));\n\n// Check if the repository exists before accessing full_name\nconst repositoryName = webhookData.repository ? webhookData.repository.full_name : 'Repository not found';\n\n// Check if the .env.staging file was changed\nconst envFileChanged = changedFiles.some(file => file.includes(envFilePath));\n\nreturn [\n  {\n    json: {\n      repository: repositoryName,\n      ref: webhookData.ref,\n      after: webhookData.after,\n      envFileChanged,\n      changedFiles\n    }\n  }\n];\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7cb9824a-13ca-4ffc-9626-cbcf5983ddc4",
      "name": "Perform Config Diff",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        280
      ],
      "parameters": {
        "jsCode": "// Only proceed if .env.staging was changed\nif (!$input.first().json.envFileChanged) {\n  return [];\n}\n\n// Get configuration values\nconst configFiles = JSON.parse($node[\"SetConfiguration\"].json[\"configFiles\"]);\nconst targetBranch = $node[\"SetConfiguration\"].json[\"targetBranch\"];\nconst cacheInvalidationKeys = JSON.parse($node[\"SetConfiguration\"].json[\"cacheInvalidationKeys\"]);\nconst repository = $input.first().json.repository;\nconst commitSha = $input.first().json.after;\n\n// Simulate diff logic for iOS\nconst envChanges = {\n  \"API_KEY\": \"new-api-key-value\",\n  \"BUNDLE_VERSION\": \"2.0.0\",\n  \"DEBUG_MODE\": \"true\"\n};\n\n// Determine which config files need updates\nconst configUpdates = [];\n\n// Check if cache invalidation is needed\nconst cacheInvalidationNeeded = cacheInvalidationKeys.some(key => envChanges.hasOwnProperty(key));\n\n// For each config file, determine what needs to be updated\nconfigFiles.forEach(file => {\n  if (file === \"Info.plist\") {\n    configUpdates.push({\n      file,\n      changes: [\n        {\n          key: \"CFBundleShortVersionString\",\n          oldValue: \"1.0.0\",\n          newValue: \"2.0.0\"\n        },\n        {\n          key: \"API_KEY\",\n          oldValue: \"old-api-key-value\",\n          newValue: \"new-api-key-value\"\n        }\n      ]\n    });\n  } else if (file === \"Config.xcconfig\") {\n    configUpdates.push({\n      file,\n      changes: [\n        {\n          key: \"API_KEY\",\n          oldValue: \"old-api-key-value\",\n          newValue: \"new-api-key-value\"\n        },\n        {\n          key: \"BUNDLE_VERSION\",\n          oldValue: \"1.0.0\",\n          newValue: \"2.0.0\"\n        }\n      ]\n    });\n  }\n});\n\nreturn [\n  {\n    json: {\n      repository,\n      commitSha,\n      targetBranch,\n      envChanges,\n      configUpdates,\n      cacheInvalidationNeeded,\n      cacheInvalidationKeys\n    }\n  }\n];"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "71c6a15a-acc5-487e-a28f-5ab7d6a06f65",
      "name": "Create Branch Name",
      "type": "n8n-nodes-base.code",
      "position": [
        860,
        280
      ],
      "parameters": {
        "jsCode": "// Create a unique branch name for the config sync\nconst data = $input.first().json;\nconst timestamp = new Date().toISOString().replace(/[:.]/g, '-');\nconst branchName = `ios-config-sync/${timestamp}`;\n\nreturn [\n  {\n    json: {\n      ...data,\n      branchName\n    }\n  }\n];"
      },
      "typeVersion": 1
    },
    {
      "id": "021ea454-007c-4e35-9396-749be82a454c",
      "name": "Invalidate Cache",
      "type": "n8n-nodes-base.code",
      "position": [
        1740,
        280
      ],
      "parameters": {
        "jsCode": "// Check if cache invalidation is needed\nconst data = $input.first().json;\n\nif (!data.cacheInvalidationNeeded) {\n  return [{ json: { ...data, cacheInvalidated: false } }];\n}\n\n// In a real implementation, you would invalidate the Xcode cache here\n// For iOS, this might involve cleaning derived data or triggering a cache cleanup\n\nreturn [\n  {\n    json: {\n      ...data,\n      cacheInvalidated: true,\n      cacheInvalidationMessage: \"Xcode cache invalidation triggered for keys: \" + data.cacheInvalidationKeys.join(\", \")\n    }\n  }\n];"
      },
      "typeVersion": 1
    },
    {
      "id": "db3f0bf4-59cf-41ff-baed-8dd994a4a9e1",
      "name": "SetConfiguration",
      "type": "n8n-nodes-base.set",
      "position": [
        200,
        280
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "envFilePath",
              "value": ".env.staging"
            },
            {
              "name": "configFiles",
              "value": "[\"Info.plist\", \"Config.xcconfig\"]"
            },
            {
              "name": "targetBranch",
              "value": "main"
            },
            {
              "name": "cacheInvalidationKeys",
              "value": "[\"API_KEY\", \"BUNDLE_VERSION\", \"ENVIRONMENT\"]"
            },
            {
              "name": "prLabels",
              "value": "[\"config-sync\", \"automated\", \"ios\"]"
            },
            {
              "name": "emailTo",
              "value": "user@example.com"
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "8b34ddf0-c670-40c1-b699-b30da3fcc6af",
      "name": "Prepare File and Merge Result",
      "type": "n8n-nodes-base.code",
      "position": [
        1300,
        280
      ],
      "parameters": {
        "jsCode": "// Prepare file updates for each iOS config file\nconst data = $input.first().json;\n\n// Extract repository name from the URL\n// Check if the URL exists\nconst url = data?.url;\nlet repoName = '';\nlet user = '';\nlet repoFullName = '';\nlet newBranch = '';\n// If the URL exists, extract the repository name and user\nif (url) {\n  const urlParts = url.split(\"/repos/\")[1]?.split(\"/\");\n  \n  if (urlParts && urlParts.length > 1) {\n    user = urlParts[0]; // Get the user (e.g., username)\n    repoName = urlParts[1]; // Get the repository name (e.g., repo_app)\n    repoFullName = `${user}/${repoName}`; // Full repository name (e.g., username/repo_app)\n  }\n}\n\nconst targetBranch = $node[\"SetConfiguration\"].json[\"targetBranch\"];\n\n// Extract the new branch name from the `ref` field\nconst ref = data?.ref;\nif (ref) {\n  // Remove \"refs/heads/\" to get just the branch name\n  newBranch = ref.replace(\"refs/heads/\", \"\");\n}\n\nreturn [\n  {\n    json: {\n      ...data,\n      user,\n      repoName,\n      repoFullName,\n      targetBranch,\n      newBranch\n    }\n  }\n];\n"
      },
      "typeVersion": 1
    },
    {
      "id": "52d9141f-0b0e-45d8-8930-3d535f99d834",
      "name": "Send Email Notification",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1960,
        280
      ],
      "parameters": {
        "sendTo": "={{ $node[\"SetConfiguration\"].json[\"emailTo\"] }}",
        "message": "=Environment Configuration Sync Completed \\nRepository: {{ $json.head.repo.full_name }}\\nBranch: {{ $('Prepare File and Merge Result').item.json.newBranch }}\\nPR: #{{ $json.number }}\\n\\nSummary:\\n- {{ $json.changed_files }} iOS config files updated\\n- {{ $json.cacheInvalidated ? 'Xcode cache invalidation triggered' : 'No cache invalidation needed' }}\\n\\nChanges Made:\\n{{ $json.configUpdates.map(u => `\\n${u.file}:\\n${u.changes.map(c => `  - ${c.key}: ${c.oldValue} \u2192 ${c.newValue}`).join('\\n')}`).join('\\n') }}\\n\\nView PR: https://github.com/{{ $json.head.repo.full_name }}/pull/{{ $json.number }}\\n\\n---\\nThis email was automatically generated by the Environment Config Sync for iOS workflow.",
        "options": {
          "appendAttribution": false
        },
        "subject": "=iOS Config Sync Completed - {{ $json.head.repo.full_name }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b218d17f-8fd4-437e-bf17-418fcd5edda6",
      "name": "Create PR",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        1520,
        280
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $json.repoFullName }}/pulls",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"title\": \"Sync iOS Configurations\",\n  \"body\": \"This PR syncs iOS configuration changes.\",\n  \"head\": \"{{ $json.newBranch }}\",\n  \"base\": \"{{ $json.targetBranch }}\",\n  \"maintainer_can_modify\": true\n}\n",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer ${token}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "githubApi"
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "a4946753-c20a-49cb-8414-ebb2786b16e1",
      "name": "Create Branch",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "position": [
        1080,
        280
      ],
      "parameters": {
        "url": "=https://api.github.com/repos/{{ $json.repository }}/git/refs",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"ref\": \"refs/heads/{{ $json[\"branchName\"] }}\",\n  \"sha\": \"{{ $json[\"commitSha\"] }}\"\n}\n",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer ${token}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "githubApi"
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e36828b0-5520-4840-9864-633cb755511a",
      "name": "Github Push Trigger",
      "type": "n8n-nodes-base.githubTrigger",
      "position": [
        -20,
        280
      ],
      "parameters": {
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "username"
        },
        "events": [
          "push"
        ],
        "options": {
          "insecureSSL": false
        },
        "repository": {
          "__rl": true,
          "mode": "list",
          "value": "repo_app",
          "cachedResultUrl": "",
          "cachedResultName": "repo_app"
        }
      },
      "credentials": {
        "githubApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7079fb26-5b6a-448e-8815-ab39231a2f8e",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -100,
        660
      ],
      "parameters": {
        "width": 2400,
        "height": 2120,
        "content": "## Description\n\n### 1. Github Push Trigger\n- Purpose: Watches for push events on the username/repo repository.\n- What it does: When a commit is pushed, it captures details such as repository name, branch (ref), commit SHA (after), and changed files.\n- Key Output: Provides raw push payload data that other nodes consume for further processing.\n\n---\n\n### 2. SetConfiguration and Customisation\n- Purpose: Defines configurable parameters for the workflow.\n- What it does:\n- Specifies the environment file to monitor (.env.staging).\n- Lists config files to update (Info.plist, Config.xcconfig).\n- Sets the target branch (main).\n- Declares cache invalidation keys (API_KEY, BUNDLE_VERSION, ENVIRONMENT).\n- Adds PR labels (config-sync, automated, ios).\n- Configures email notification recipient (eg: ios-team@example.com).\n- Key Output: Passes configuration values downstream for diffing and PR creation.\n\n---\n\n### 3. Check Changed Files\n- Purpose: Detects if .env.staging was modified in the latest push.\n- What it does:\n- Parses commit payload.\n- Flattens all added/modified/removed files.\n- Checks whether .env.staging exists in the changed files.\n- Key Output: Returns envFileChanged = true/false plus the repository and commit details.\n\n---\n\n### 4. Perform Config Diff\n- Purpose: Analyzes differences between .env.staging and iOS configuration files.\n- What it does:\n- Extracts simulated environment changes (e.g., API_KEY, BUNDLE_VERSION).\n- Prepares configUpdates array describing old \u2192 new values for each file.\n- Determines if cache invalidation is required (based on keys).\n- Key Output: Provides structured diff results and flags whether cache invalidation is needed.\n\n---\n\n### 5. Create Branch Name\n- Purpose: Generates a unique branch name for the configuration changes.\n- What it does: Appends a timestamp to ios-config-sync/ (e.g., ios-config-sync/2025-09-01T12-00-00).\n- Key Output: New branch name (branchName) to be created in GitHub.\n\n---\n\n### 6. Create Branch\n- Purpose: Creates a new branch in the repository.\n- What it does: Calls GitHub API to create a reference (refs/heads/branchName) from the latest commit SHA.\n- Key Output: Confirmation of branch creation for downstream file updates.\n\n---\n\n### 7. Prepare File and Merge Result\n- Purpose: Prepares repository details for file updates and PR creation.\n- What it does:\n- Extracts user, repoName, and repoFullName from the repository URL.\n- Determines newBranch name from ref.\n- Prepares a merged JSON object with config updates and metadata.\n- Key Output: Supplies enriched repository details for PR step.\n\n---\n\n### 8. Create PR\n- Purpose: Opens a pull request in GitHub with the config changes.\n- What it does:\n- Uses GitHub API to create PR with title \u201cSync iOS Configurations\u201d.\n- Sets head = newBranch, base = main.\n- Attaches PR labels.\n- Key Output: Returns PR number (prNumber) and metadata for notifications.\n\n---\n\n### 9. Invalidate Cache\n- Purpose: Triggers Xcode build cache invalidation if required.\n- What it does:\n- Checks cacheInvalidationNeeded.\n- If true, marks cache as invalidated and logs message (e.g., \u201cXcode cache invalidation triggered for keys: API_KEY, BUNDLE_VERSION\u201d).\n- Key Output: Adds cacheInvalidated status and message to workflow data.\n\n---\n\n### 10. Send Email Notification\n- Purpose: Notifies stakeholders of completed config sync.\n- What it does:\n- Sends Gmail notification to the configured recipient.\n- Includes repository name, branch, PR number, number of updated config files, cache invalidation status, and detailed change log.\n- Adds GitHub PR link for quick access.\n- Key Output: Final communication to confirm sync completion.\n\n---"
      },
      "typeVersion": 1
    },
    {
      "id": "d8620a5d-87e5-4174-978e-bbd37bce0a50",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -100,
        120
      ],
      "parameters": {
        "width": 2400,
        "height": 440,
        "content": "## iOS Environment Config Sync Wizard: .env to Xcode"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f44fd78d-b44c-4689-adf7-f576535c6db5",
  "connections": {
    "Create PR": {
      "main": [
        [
          {
            "node": "Invalidate Cache",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Branch": {
      "main": [
        [
          {
            "node": "Prepare File and Merge Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Invalidate Cache": {
      "main": [
        [
          {
            "node": "Send Email Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SetConfiguration": {
      "main": [
        [
          {
            "node": "Check Changed Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Branch Name": {
      "main": [
        [
          {
            "node": "Create Branch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Changed Files": {
      "main": [
        [
          {
            "node": "Perform Config Diff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Github Push Trigger": {
      "main": [
        [
          {
            "node": "SetConfiguration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Perform Config Diff": {
      "main": [
        [
          {
            "node": "Create Branch Name",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare File and Merge Result": {
      "main": [
        [
          {
            "node": "Create PR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

iOS Environment Config Sync Wizard: .env to Xcode. Uses gmail, httpRequest, githubTrigger. Event-driven trigger; 12 nodes.

Source: https://github.com/weblineindia/n8n-Automate-iOS-config-sync-.env-to-Xcode-with-GitHub-PRs-and-email/blob/main/main.json — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

Deliver your product updates in a modern, accessible format. This workflow automatically transforms GitHub releases into podcast-style audio announcements and distributes them via email and Slack.

Github Trigger, Notion, OpenAI +5
Email & Gmail

Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.

HTTP Request, Execute Workflow Trigger, Gmail +1
Email & Gmail

Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.

HTTP Request, Execute Workflow Trigger, Gmail +1
Email & Gmail

Echo Brand Voice Analysis (Processor) - TASK-074 Dec 10 Fix. Uses formTrigger, httpRequest, executeWorkflowTrigger, moveBinaryData. Event-driven trigger; 40 nodes.

Form Trigger, HTTP Request, Execute Workflow Trigger +2
Email & Gmail

AICARE Email Blast System. Uses googleDrive, httpRequest, googleSheets, gmail. Event-driven trigger; 39 nodes.

Google Drive, HTTP Request, Google Sheets +2