Level Up/DevOps & Delivery

How to Set Up CI/CD for MuleSoft with GitHub Actions.

The 2026 stack: Connected Apps, CloudHub 2.0, JDK 17, reusable workflows, and Anypoint CLI v4 for the cases Maven cannot reach.

How-to guideMuleSoftGitHub ActionsJune 202615 min read
dispatch://mulesoftHT-03
How to Set Up CI/CD for MuleSoft with GitHub Actions. MuleSoft
June 2026 · 15 min readHow-to guide

Every time a developer runs mvn deploy from their local machine, the deployment carries that developer's Maven settings, their JDK version, their local environment, and whatever credentials they happen to have authenticated with. One misconfigured property file is the difference between a clean release and a production incident on a Friday evening.

CI/CD is not optional in 2026. The question is what shape it takes. This guide walks through the modern shape: a GitHub Actions pipeline that authenticates to Anypoint Platform with a Connected App, builds on JDK 17 against Mule 4.9, runs MUnit with a coverage gate, and deploys to CloudHub 2.0. The workflow itself lives once in a central repository as a reusable workflow, and every API repository in the estate calls it. Production deployments wait behind a GitHub Environment with required reviewers. Anypoint CLI v4 handles the things the Mule Maven Plugin cannot reach.

The article covers two halves. The first: the modern default (Connected Apps, CloudHub 2.0, reusable workflows, JDK 17). The second: the legacy path (CloudHub 1.0, username/password authentication, Mule Maven Plugin 3.x) and what migrating off it looks like. Most existing clients still have one foot in that world.

The 2026 stack

Four decisions define a modern MuleSoft CI/CD pipeline. Each is a deliberate move away from how pipelines were built in 2020, and each is the default for any new programme started today.

Connected App, not username and password

Connected Apps are MuleSoft's machine-to-machine identity model, configured under Access Management with a fixed set of scopes and a client_id plus client_secret. The Mule Maven Plugin 4.x accepts Connected App credentials directly. Username and password authentication is being progressively deprecated and does not survive an Anypoint Platform tenant that has MFA enabled, which is most of them after 2024. Even where it still works, Salesforce's published guidance is to move new pipelines to Connected Apps.

CloudHub 2.0 for new applications

CloudHub 2.0 uses Kubernetes underneath, supports finer-grained vCore sizing (0.1 / 0.2 / 0.5 vCore), allows multiple replicas per application without the worker-cluster pricing penalty, and exposes a simpler deployment model via the cloudhub2Deployment XML block. CloudHub 1.0 is still in support but is on a documented deprecation path. New applications go to CloudHub 2.0 unless there is a specific connector or runtime feature that has not landed on the new platform.

JDK 17 and Mule 4.9

Mule 4.9 (released 2025) made JDK 17 the recommended default. The published release uses a java17-tagged artifact identifier. The CI runner must install JDK 17 from a TCK-certified distribution (Temurin is the Ampleshift default) and the pom.xml must declare app.runtime as 4.9-java17 or later.

Reusable workflows in a central repository

Copy-pasting the same 80-line workflow file into thirty API repositories is the worst way to scale a CI/CD practice. The right pattern: a reusable workflow defined once in a central repository, called by every API repo. Updates ship once; every API picks them up at the next build. GitHub's workflow_call trigger is the mechanism.

Ampleshift CI/CD baseline

Connected App per environment, CloudHub 2.0, JDK 17, Mule 4.9, one reusable workflow per organisation, Anypoint CLI v4 for advanced operations, GitHub Environments for production approval. Anything new diverges from this for a documented reason, or not at all.

Prerequisites

RequirementDetails
Anypoint Platform accountAccess Management permission to create Connected Apps. CloudHub 2.0 entitlement on at least one environment.
GitHub repositoryPrivate repo on a paid plan, or a public repo on any plan. Free private repos do not have GitHub Actions or Environments. One central CI/CD repository plus one per API.
MuleSoft Nexus credentialsRequired to resolve Mule Enterprise dependencies. Raise a MuleSoft support ticket if your organisation has a subscription but no Nexus credentials.
Mule Maven PluginVersion 4.1.1 or later for CloudHub 2.0 with the cloudhub2Deployment block. The 3.x line works only for CloudHub 1.0.
Anypoint CLI v4Optional for build and deploy; required for operations beyond the Mule Maven Plugin. Install: npm install -g anypoint-cli-v4
Branch protectionRequired reviewers on main and uat branches. Status check requirement: the build-test job must pass before merge. Set in Repository Settings before the first pipeline run.

Step 1: set up the Connected App with least-privilege scopes

One Connected App per environment is the Ampleshift default. The Sandbox app deploys to Sandbox, the UAT app deploys to UAT, the Production app deploys to Production. Sharing one app across environments works but produces a blast radius that is hard to defend during an audit.

Sign in to Anypoint Platform, navigate to Access Management > Connected Apps, and click Create App. Choose App acts on its own behalf (client credentials). Add scopes scoped to the business group and the target environment. The minimum scopes for a CloudHub 2.0 deployment pipeline:

After saving, copy the Client ID and the Client Secret. The secret is shown once; if you lose it, regenerate. Store the values directly into GitHub Secrets at the environment level (covered in Step 2), not at the repository level: a Production Connected App secret at the repository level leaks into Sandbox and UAT runs.

CRITICAL: never reuse a Production Connected App in a non-production pipeline

Three environments, three Connected Apps. The Sandbox pipeline must not be able to authenticate as the Production identity, even by accident. This is the single highest-value control you can set in the first hour of any new CI/CD programme.

Step 2: configure repository secrets, variables, and environments

GitHub has three places to put configuration: repository secrets, repository variables, and environment-scoped secrets and variables. The default mistake is to put everything at the repository level.

Environment-scoped secrets (one per env): Create three GitHub Environments: sandbox, uat, prod. On each environment add the Connected App credentials and any environment-specific values. Environment secrets are only available to workflow runs that target that environment.

# Per environment (Settings > Environments > <env-name> > Add secret)
ANYPOINT_CLIENT_ID     # Connected App client ID for this environment
ANYPOINT_CLIENT_SECRET  # Connected App client secret for this environment

# Environment variables (non-secret, but env-specific)
ANYPOINT_ENV    # 'Sandbox' | 'UAT' | 'Production'
CLOUDHUB_REGION # e.g. 'eu-west-1', 'us-east-2'
REPLICAS        # e.g. '1' for sandbox, '2' for prod

Repository-scoped secrets (shared across envs): Secrets that are genuinely shared across environments stay at the repository level. The Nexus credentials are the canonical case.

# Repository (Settings > Secrets and variables > Actions > Secrets)
NEXUS_USERNAME   # MuleSoft Enterprise Nexus username
NEXUS_PASSWORD   # MuleSoft Enterprise Nexus password
ANYPOINT_ORG_ID  # Organisation ID (constant across envs)

# Repository variables
APP_NAME             # Base application name (env suffix added at deploy)
MULE_RUNTIME_VERSION # e.g. '4.9.0' (also pinned in pom.xml)

TIP: required reviewers on the prod environment

On the prod GitHub Environment, configure Required reviewers and set the deployment-branch rule to main only. The pipeline blocks at the deployment step until a named approver clicks Approve. This is the simplest production approval gate in the industry and it costs nothing to configure.

Step 3: author the Maven settings.xml for the runner

The Mule Maven Plugin needs Maven to know how to reach the MuleSoft Enterprise Nexus and Anypoint Exchange. Local developers configure this in their ~/.m2/settings.xml. The CI runner uses a settings.xml committed to the repository under .maven/settings.xml. Credentials come from environment variables, never inline.

<!-- .maven/settings.xml -->
<settings>
  <servers>
    <server>
      <id>MuleRepository</id>
      <username>${env.NEXUS_USERNAME}</username>
      <password>${env.NEXUS_PASSWORD}</password>
    </server>
    <server>
      <id>anypoint-exchange-v3</id>
      <username>~~~Client~~~</username>
      <password>${env.ANYPOINT_CLIENT_ID}~?~${env.ANYPOINT_CLIENT_SECRET}</password>
    </server>
  </servers>
</settings>

The anypoint-exchange-v3 username/password format requires the Connected App as username ~~~Client~~~ with the password formatted as <client_id>~?~<client_secret>. The triple-tilde and ~?~ separator are the documented convention for client-credentials auth against Exchange.

Step 4: configure the pom.xml for Connected App and CloudHub 2.0

The pom.xml does three things: pins the Mule runtime, configures the Mule Maven Plugin for CloudHub 2.0 with Connected App authentication, and declares the MUnit Maven Plugin with the coverage gate.

Properties and runtime pinning:

<properties>
  <app.runtime>4.9-java17</app.runtime>
  <mule.maven.plugin.version>4.1.1</mule.maven.plugin.version>
  <munit.version>3.3.0</munit.version>
  <java.version>17</java.version>
</properties>

Mule Maven Plugin with CloudHub 2.0 deployment:

<plugin>
  <groupId>org.mule.tools.maven</groupId>
  <artifactId>mule-maven-plugin</artifactId>
  <version>${mule.maven.plugin.version}</version>
  <extensions>true</extensions>
  <configuration>
    <cloudhub2Deployment>
      <uri>https://anypoint.mulesoft.com</uri>
      <environment>${env.ANYPOINT_ENV}</environment>
      <muleVersion>4.9.0</muleVersion>
      <releaseChannel>NONE</releaseChannel> <!-- pin runtime exactly -->
      <applicationName>${app.name}-${env.ANYPOINT_ENV}</applicationName>
      <replicas>${env.REPLICAS}</replicas>
      <vCores>0.1</vCores>
      <connectedAppClientId>${env.ANYPOINT_CLIENT_ID}</connectedAppClientId>
      <connectedAppClientSecret>${env.ANYPOINT_CLIENT_SECRET}</connectedAppClientSecret>
      <connectedAppGrantType>client_credentials</connectedAppGrantType>
    </cloudhub2Deployment>
  </configuration>
</plugin>

CRITICAL: releaseChannel NONE pins the runtime exactly

releaseChannel set to NONE with an explicit muleVersion means CloudHub 2.0 deploys exactly that runtime, no auto-upgrade. The alternative (EDGE, LTS) lets MuleSoft choose patch versions for you. Fine for sandboxes; dangerous for production. Pin in production; allow drift in sandbox if you want.

Step 5: build the reusable workflow in a central repository

Stand up one central repository (the Ampleshift convention is org-name/ci-cd-workflows) that owns the reusable workflow. Every API repository in the estate calls it via workflow_call. Updates to the build, test, and deploy logic ship from one place. This is the single biggest scaling decision in a multi-API MuleSoft estate.

# .github/workflows/mulesoft-cicd.yml in the central CI/CD repo
name: MuleSoft CI/CD (reusable)
on:
  workflow_call:
    inputs:
      app_name: { type: string, required: true }
      target_env: { type: string, required: true } # sandbox|uat|prod
      mule_version: { type: string, default: '4.9.0' }
    secrets:
      ANYPOINT_CLIENT_ID: { required: true }
      ANYPOINT_CLIENT_SECRET: { required: true }
      NEXUS_USERNAME: { required: true }
      NEXUS_PASSWORD: { required: true }

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: 17
          cache: maven
      - name: Build and run MUnit with coverage
        run: mvn -B clean verify -s .maven/settings.xml
        env:
          NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
          NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
          ANYPOINT_CLIENT_ID: ${{ secrets.ANYPOINT_CLIENT_ID }}
          ANYPOINT_CLIENT_SECRET: ${{ secrets.ANYPOINT_CLIENT_SECRET }}

  deploy:
    needs: build-test
    runs-on: ubuntu-latest
    environment: ${{ inputs.target_env }} # ties to GitHub Environment with approval
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: 'temurin', java-version: 17, cache: maven }
      - name: Deploy to CloudHub 2.0
        run: |
          mvn -B deploy -DmuleDeploy -DskipTests \
            -s .maven/settings.xml \
            -Dapp.name=${{ inputs.app_name }}
        env:
          ANYPOINT_ENV: ${{ vars.ANYPOINT_ENV }}
          REPLICAS: ${{ vars.REPLICAS }}
          ANYPOINT_CLIENT_ID: ${{ secrets.ANYPOINT_CLIENT_ID }}
          ANYPOINT_CLIENT_SECRET: ${{ secrets.ANYPOINT_CLIENT_SECRET }}
          NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
          NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}

Step 6: call the reusable workflow from each API repository

With the central workflow in place, each API repository's own .github/workflows file shrinks to a few lines. Branch-to-environment routing happens here. The pattern below routes develop to sandbox, uat to UAT, and main to production.

# .github/workflows/ci.yml in each API repo
name: API CI/CD
on:
  push:
    branches: [ 'develop', 'uat', 'main' ]
  pull_request:
    branches: [ 'develop', 'uat', 'main' ]

jobs:
  pipeline:
    uses: org-name/ci-cd-workflows/.github/workflows/mulesoft-cicd.yml@v1
    with:
      app_name: customer-experience-api
      target_env: ${{ github.ref == 'refs/heads/main' && 'prod' || github.ref == 'refs/heads/uat' && 'uat' || 'sandbox' }}
    secrets:
      ANYPOINT_CLIENT_ID: ${{ secrets.ANYPOINT_CLIENT_ID }}
      ANYPOINT_CLIENT_SECRET: ${{ secrets.ANYPOINT_CLIENT_SECRET }}
      NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
      NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}

TIP: pin the reusable workflow to a tag, not a branch

@v1 (a tag) is reproducible. @main (a branch) is not: an API's build can break because somebody pushed an incompatible change to the central repo. Tag the reusable workflow, bump the tag deliberately, communicate the upgrade. Same discipline as semantic versioning for any other shared dependency.

Step 7: run MUnit with coverage gates

MUnit produces coverage reports as a side effect of running mvn verify. Three thresholds matter and they are not the same.

ThresholdTargetWhat it measures
requiredFlowCoverage100%Every flow has at least one MUnit test that executes it. Easy to hit; the strongest single coverage signal.
requiredResourceCoverage80%Percentage of XML configuration files exercised by tests. Catches whole-file branches that have no tests at all.
requiredApplicationCoverage80%Percentage of message processors. Harder to hit; the realistic signal of test depth.

Set failBuild to true on all three. A failing coverage threshold should break the build the same way a failing test does. The argument against this is always “we will fix the coverage later” and later never arrives.

Step 8: wire Anypoint CLI v4 for what Maven cannot do

The Mule Maven Plugin handles build, package, and deploy. It does not handle: updating API Manager with the deployed application's URL, publishing or promoting Exchange assets across organisations, querying Runtime Manager for deployed application state, or rotating client credentials on contracts. Anypoint CLI v4 covers all of these.

- name: Install Anypoint CLI v4
  run: npm install -g anypoint-cli-v4

- name: Update API Manager with deployed URL
  run: |
    appName=$(mvn help:evaluate -Dexpression=project.name -q -DforceStdout)
    appId=$(anypoint-cli-v4 runtime-mgr:application:list --output json \
      | jq -r '.[]|select(.name=='''$appName''').id')
    appUri=$(anypoint-cli-v4 runtime-mgr:application:describe --output json "$appId" \
      | jq -r '.target.deploymentSettings.http.inbound.publicUrl')
    anypoint-cli-v4 api-mgr:api:edit --uri "$appUri" "${{ vars.API_MANAGER_ID }}"
  env:
    ANYPOINT_CLIENT_ID: ${{ secrets.ANYPOINT_CLIENT_ID }}
    ANYPOINT_CLIENT_SECRET: ${{ secrets.ANYPOINT_CLIENT_SECRET }}
    ANYPOINT_ORG: ${{ secrets.ANYPOINT_ORG_ID }}
    ANYPOINT_ENV: ${{ vars.ANYPOINT_ENV }}

Step 9: production approval gates with GitHub Environments

A pipeline that auto-deploys to production is faster than one that does not. It is also one bad merge away from a service-impact incident. The middle ground: automatic deployment to sandbox and UAT, with production gated behind a named human approval. GitHub Environments deliver this at no cost.

Configure the prod environment: Settings > Environments > New environment > Name: prod. Add Required reviewers (between 1 and 6). Set deployment branches to main only. Add the environment-scoped Connected App secrets here, not at the repository level.

When the reusable workflow's deploy job declares environment: prod, GitHub pauses the run at the start of that job. A banner appears in the run page asking the named reviewers to approve. The deployment does not start until at least one reviewer clicks Approve. The approver's identity is captured in the run history.

NOTE: reviewers must be repository collaborators

GitHub Environments require approvers to have at least Read access to the repository. For multi-org setups where the on-call rotation lives in a different organisation, add them as outside collaborators with Read access. Giving the on-call team Write access to gain approval rights widens the blast radius unnecessarily.

Step 10: migration path from CloudHub 1.0 and username/password

Most existing clients still have one foot in the legacy stack: CloudHub 1.0 workers, the Mule Maven Plugin 3.x line, username/password authentication, and a pipeline assembled when JDK 8 was the default. None of this is broken today. All of it is on a deprecation timeline.

What the legacy stack looks like:

Migration in four steps:

Step 1: Create one Connected App per environment alongside the existing username/password setup. Run both auth methods in parallel for two weeks. Verify the Connected App works end to end before retiring the credentials.

Step 2: Upgrade the Mule Maven Plugin to 4.1.1+ on a single non-critical API. Switch the cloudHubDeployment block to cloudhub2Deployment. Deploy to CloudHub 2.0 in Sandbox. Validate behaviour, including custom properties, secure properties, and connector configurations.

Step 3: Bump the runtime: app.runtime to 4.9-java17 and the runner JDK to 17. Re-run the MUnit suite. Classloader and date/time API changes between JDK 8/11 and 17 are the most common failure points.

Step 4: Extract the workflow logic into the central reusable workflow. Migrate one repository at a time. Decommission the per-repo workflow files as each repository is migrated.

TIP: do not migrate every API at once

Migrate two or three lower-risk APIs first, learn what breaks, then write the migration playbook for the rest. The team migrating last benefits most; the team migrating first absorbs the unknowns. Stagger deliberately.

Troubleshooting and common pitfalls

SymptomRoot cause and fix
401 Unauthorized on deployConnected App scopes too narrow, or scoped to the wrong business group/environment. Review scopes from Step 1. Most commonly missed: Manage Application Data.
404 on anypoint-exchange-v3Wrong Exchange version ID in settings.xml. Use anypoint-exchange-v3, not v2. The v2 endpoint returns 404 for client-credentials operations.
Passes locally, fails on runnerJDK mismatch. The runner is probably on JDK 11 while the developer is on JDK 17. Pin actions/setup-java@v4 with java-version: 17 explicitly.
MUnit timezone assertion failuresLocal TZ is non-UTC; the runner is UTC. Freeze test inputs in UTC or set TZ: 'UTC' in the workflow env block.
CH2 deploy hangs at “application starting”Application failing to start at runtime. The plugin exits success at deploy submission, not on healthy state. Add a post-deploy step that polls anypoint-cli-v4 runtime-mgr:application:describe until status == STARTED.
Reusable workflow change broke 20 APIsCallers used @main instead of @v1. Tag the reusable workflow and point callers at the tag. Treat the workflow's tag like a semver release for a library.
Production deployed without approvalEither the prod GitHub Environment has no required reviewers configured, or the deploy job did not declare environment: prod. Both must be in place.
Nexus 403 ForbiddenSubscription expired, credentials wrong, or entitlement does not include Enterprise repository access. Raise a MuleSoft support ticket.

What you have built

A GitHub Actions pipeline for MuleSoft that is opinionated, scalable, and aligned with where Salesforce is pushing the platform: Connected Apps for authentication, CloudHub 2.0 as the deployment target, JDK 17 with Mule 4.9 on the runner, MUnit with coverage gates that break the build, a reusable workflow in a central repository, Anypoint CLI v4 for operations Maven cannot reach, and GitHub Environments for production approval.

The single most important operational habit is the per-environment Connected App. Three identities, three secret scopes, three GitHub Environments. Get this right in the first hour and every other decision becomes safer. Get it wrong and a Sandbox pipeline run can authenticate as the Production identity, which is the most common cause of avoidable production incidents in MuleSoft CI/CD reviews.

Key takeaways

This pipeline pattern is part of the Ampleshift CI/CD accelerator baseline, one of the reasons our programmes reach production with consistent, auditable delivery pipelines from day one.

Continue reading

Want us in your next story?

Connect with us today and let's turn your integration challenges into opportunities for growth.

Book a call