CI/CD Pipeline for a MuleSoft App
Automate build, test, and deploy of a Mule API to CloudHub 2.0 with GitHub Actions.
Introduction
30 Second Summary
Every time you update a MuleSoft app, someone has to remember to run the tests, package it, and deploy it manually. One missed step and production breaks. One forgotten config and the whole team is blocked.
In this project, you will build a CI/CD pipeline using GitHub Actions that automatically builds, tests, and deploys a MuleSoft API to CloudHub 2.0 every time you push code to GitHub.
What You'll Build
You will push code to your GitHub repository and watch it flow through automated tests to a live, rate-limited API on CloudHub 2.0 without touching a single button.
By the end of this project, you'll have:
- A GitHub Actions workflow that automatically runs MUnit tests, packages your Mule app, and deploys it to CloudHub 2.0 on every push.
- First-hand experience of the pipeline catching a broken test, turning red, and blocking deployment until you fix it.
- A managed API Manager instance with a rate limiting policy that enforces request quotas and returns HTTP 429 when exceeded.
- Secret Mission: Replace basic credentials with a Connected App using OAuth 2.0 client credentials for production-grade CI/CD security.
Are there any prerequisites?
This project assumes you have an Anypoint Platform account, Anypoint Studio installed, Maven installed, and a GitHub account.
If you do not have these set up yet, create your free accounts and install the tools before starting Step 1.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up the GitHub Repository and Mule Project
Before you can automate builds and deployments, you need something to build and deploy. A working Mule application living in a version-controlled repository is the foundation everything else in this project sits on.
In this step, you will create a simple HTTP API in Anypoint Studio, verify it responds locally, then push it to a GitHub repository so it is ready for automation.
In this step, get ready to:
- Create a Mule 4 HTTP API in Anypoint Studio.
- Run the app locally and verify it responds with JSON.
- Push the project to a public GitHub repository.
Create the Mule project in Anypoint Studio
Your CI/CD pipeline needs an application to operate on. You will build a minimal HTTP endpoint that returns a JSON greeting.
- In Anypoint Studio, click File in the menu bar.
- Select New, then Mule Project.
- In the Project Name field, enter greeting.
- Leave all other defaults and click Finish.
Studio creates the project and opens a blank canvas with an empty flow file at src/main/mule/greeting.xml.
- In the Mule Palette tab on the right, click HTTP.
- Drag the Listener component onto the canvas.
- In the Listener properties tab at the bottom, click the green + icon next to Connector configuration.
- In the dialog that opens, confirm Host is 0.0.0.0 and Port is 8081. Click OK.
- Back in the Listener properties, set the Path field to /greeting.
- In the Mule Palette, click Core.
- Drag the Set Payload component to the right of the Listener on the canvas (into the Process area).
- Click the Set Payload component to open its properties.
- Make sure the fx button is deselected (not highlighted).
- In the Value field, type:
{ "message": "Hello from MuleSoft!" }
- In the MIME Type dropdown (under the Advanced section of Set Payload properties), select application/json.
What did you just build?
You created a Mule flow with two components. The HTTP Listener waits for incoming requests on port 8081 at path /greeting. When a request arrives, the Set Payload transformer responds with the JSON body you typed.
Now run the app locally to confirm it works.
- Right-click the canvas and select Run project greeting.
- Watch the Console tab at the bottom of Studio. Wait until you see a message containing DEPLOYED.
Before you test the endpoint, what do you expect the response body to contain?
- Open a terminal (outside of Studio) and send a GET request to your running app by running this command:
curl http://localhost:8081/greeting
You should see the following JSON response:
{ "message": "Hello from MuleSoft!" }
Not seeing a response?
- Confirm the Studio Console shows DEPLOYED before you send the request.
- If you see a connection refused error, check that port 8081 is not already in use by another application.
- Make sure the Path in your Listener is exactly /greeting (with the leading slash).
Help me troubleshoot why my Mule app is not responding to requests on localhost:8081/greeting.
Create a GitHub repository and clone it
Your working app needs a home in version control so GitHub Actions can access it later.
- In your browser, navigate to github.com.
- Click the + icon in the top right corner and select New repository.
- Enter a repository name (for example, mulesoft-greeting-cicd).
- Set the visibility to Public.
- Check Add a README file.
- Click Create repository.
GitHub creates the repository and shows you the new repo page with a README.md file.
- Copy the repository URL by clicking the green Code button and copying the HTTPS URL.
- Open a terminal and move to your Desktop by running this command:
cd ~/Desktop
- Clone your repository by running:
git clone [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/mulesoft-greeting-cicd.git"]]
- Move into the cloned repository by running:
cd [[REPO_NAME="mulesoft-greeting-cicd"]]
Now you need to copy your Mule project files into this repository folder.
- In Anypoint Studio, find your project's location on disk. Right-click the greeting project in the Package Explorer, select Properties, and note the Location path shown.
- Copy all files from that project folder into your cloned repository. Use your file manager (Finder on macOS or File Explorer on Windows) to copy the contents (pom.xml, src/, and all other files) into the mulesoft-greeting-cicd folder on your Desktop.
Use the terminal instead
On macOS or Linux, you can copy with a terminal command. Replace the path with the Location you noted from Studio:
cp -r [[MULE_PROJECT_PATH="/path/to/AnypointStudio/workspace/greeting"]]/* .
- Confirm the files were copied by running:
ls
You should see pom.xml, src, and README.md in the output.
Don't see pom.xml?
- Double-check the Location path in Studio Properties. The project root is the folder containing pom.xml.
- Make sure you copied the contents of the project folder, not the folder itself (you want pom.xml at the repo root, not inside a nested greeting/ subdirectory).
Help me figure out why my Mule project files did not copy correctly into my Git repository folder.
Add a .gitignore and push to GitHub
Before committing, you need to exclude build artifacts and IDE files that should not be tracked in version control.
- Create a .gitignore file in the repository root by running:
cat << 'EOF' > .gitignore
target/
.mule/
.idea/
*.iml
EOF
What does this .gitignore do?
- target/ excludes Maven build output (compiled JARs and temporary files).
- .mule/ excludes the local Mule runtime cache.
- .idea/ and *.iml exclude IntelliJ/Studio IDE metadata that is specific to your machine.
- Confirm the file was created by running:
cat .gitignore
You should see the four exclusion patterns printed.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all files before committing.
ⓧ I'd like to double check the full code
Your .gitignore should contain exactly:
target/
.mule/
.idea/
*.iml
Your src/main/mule/greeting.xml (generated by Studio) should look similar to:
<mule xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:http="http://www.mulesoft.org/schema/mule/http" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd">
<http:listener-config name="HTTP_Listener_config" doc:name="HTTP Listener config">
<http:listener-connection host="0.0.0.0" port="8081" />
</http:listener-config>
<flow name="greetingFlow">
<http:listener doc:name="Listener" config-ref="HTTP_Listener_config" path="/greeting" />
<set-payload value='{ "message": "Hello from MuleSoft!" }' mimeType="application/json" doc:name="Set Payload" />
</flow>
</mule>
Now commit and push everything to GitHub.
- Stage all files by running:
git add .
- Commit the project by running:
git commit -m "Initial commit: Mule greeting API"
- Push to GitHub by running:
git push origin main
Before you check the result, what files do you expect to see in the GitHub repository?
- In your browser, refresh your GitHub repository page.
You should see pom.xml, src/, .gitignore, and README.md listed in the repository file tree.
Push rejected or files missing?
- If the push is rejected, your local branch may need to pull the README first. Run git pull --rebase origin main then push again.
- If pom.xml or src/ are missing on GitHub, verify you ran git add . from the repository root (the folder containing pom.xml).
Help me fix a git push rejection or missing files when pushing my Mule project to GitHub.
Your Mule app is live locally and your code is safely in GitHub. Next up, you will create a GitHub Actions workflow that automatically builds and tests this project every time you push.
Write the CI Workflow (Build and Test)
Your Mule project lives on GitHub, but right now pushing code does nothing automatically. There is no feedback loop telling you whether the code compiles, whether dependencies resolve, or whether your MUnit tests still pass.
GitHub Actions lets you define automation workflows as YAML files inside your repository. When code is pushed, GitHub reads these files and executes the steps you define on a cloud runner. In this step, you will create a workflow that builds and tests your Mule application on every push.
In this step, get ready to:
- Create a GitHub Actions workflow file that triggers on pushes to the main branch.
- Configure Maven to access the MuleSoft Enterprise repository using GitHub Secrets.
- Push the workflow and confirm it runs green in the Actions tab.
Create the workflow file
GitHub Actions workflows live in a special directory at the root of your repository: .github/workflows/. Any YAML file in this directory becomes a workflow that GitHub can trigger automatically.
- In your project's root folder (the same directory that contains your pom.xml), create the directory path .github/workflows/ by running this command in your terminal:
mkdir -p .github/workflows
- Create a new file called build.yml inside .github/workflows/.
- Open build.yml in your editor and paste the following workflow definition:
name: CI Pipeline
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build and Test
run: mvn clean test
What does this workflow do?
- on: push: branches: [main] tells GitHub to run this workflow every time code is pushed to the main branch.
- runs-on: ubuntu-latest gives you a fresh Linux runner with 4 CPU and 16 GB RAM, free for public repositories.
- actions/checkout@v4 clones your repository code onto the runner.
- actions/setup-java@v4 installs Java 17 (Temurin distribution) and caches your Maven dependencies for faster future runs.
- mvn clean test compiles your Mule application and runs all MUnit tests.
- Save build.yml.
- Confirm the file exists by running:
ls .github/workflows/
You should see build.yml listed in the output.
Don't see the file?
- Make sure you created both the .github directory and the workflows subdirectory inside it. The dot prefix on .github makes it a hidden folder on macOS and Linux.
- Run ls -a to show hidden files if you cannot see the .github folder.
Help me create my GitHub Actions workflow file.
Configure MuleSoft repository access and push the workflow
Your Mule application depends on libraries hosted in the MuleSoft Enterprise repository. The GitHub Actions runner does not have access to this repository by default, so you need to generate a Maven settings.xml that provides the credentials.
- In build.yml, find the line that says - name: Build and Test.
- Add a new step directly above it that generates the settings.xml file. Copy the following code and paste it between the Set up Java 17 step and the Build and Test step:
- name: Generate Maven settings.xml
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings>
<servers>
<server>
<id>MuleRepository</id>
<username>${{ secrets.MULE_EE_USERNAME }}</username>
<password>${{ secrets.MULE_EE_PASSWORD }}</password>
</server>
</servers>
<profiles>
<profile>
<id>mule-extra-repos</id>
<activation><activeByDefault>true</activeByDefault></activation>
<repositories>
<repository>
<id>MuleRepository</id>
<url>https://repository.mulesoft.org/releases/</url>
</repository>
</repositories>
</profile>
</profiles>
</settings>
EOF
What does this step do?
This step creates a Maven settings.xml on the runner before the build runs. It points Maven to the MuleSoft Enterprise releases repository at https://repository.mulesoft.org/releases/ and authenticates using credentials stored in GitHub Secrets.
The ${{ secrets.MULE_EE_USERNAME }} syntax tells GitHub to inject the secret value at runtime without ever exposing it in logs.
- Save build.yml.
✔️ Awesome, I've got everything!
Great. Double check you have saved build.yml with both the Generate Maven settings.xml step and the Build and Test step in the correct order.
ⓧ I'd like to double check the full code
Scroll up and confirm your build.yml contains these sections in order:
- The name: CI Pipeline header and on: push trigger block.
- The Checkout code step using actions/checkout@v4.
- The Set up Java 17 step using actions/setup-java@v4 with temurin distribution and maven cache.
- The Generate Maven settings.xml step with the heredoc block writing the settings.xml file to ~/.m2/.
- The Build and Test step running mvn clean test.
Now you need to store your Anypoint Platform credentials as GitHub Secrets so the workflow can access them at runtime.
- Open your GitHub repository in your browser.
- Click the Settings tab in the repository header.
- In the left sidebar under the Security section, click Secrets and variables, then click Actions.
- Click the Secrets tab.
- Click New repository secret.
- In the Name field, enter MULE_EE_USERNAME.
- In the Secret field, enter your Anypoint Platform username (the email you use to log in).
- Click Add secret.
- Click New repository secret again.
- In the Name field, enter MULE_EE_PASSWORD.
- In the Secret field, enter your Anypoint Platform password.
- Click Add secret.
You should now see both MULE_EE_USERNAME and MULE_EE_PASSWORD listed on the Actions secrets page.
Why use GitHub Secrets?
GitHub Secrets are encrypted values that your workflow can access at runtime. They never appear in logs or in your repository code. This keeps your credentials secure while still allowing the CI runner to authenticate with the MuleSoft repository.
🙋♀️ Don't see the Settings tab?
The Settings tab only appears if you are the repository owner or have admin access. If you created the repo under your own account, it should be visible. Look for a dropdown menu (...) next to the repository tabs if the tab is hidden.
Help me find the repository settings to add secrets.
With secrets configured, push your workflow file to trigger the first pipeline run.
- Commit and push your changes by running these commands:
git add .github/workflows/build.yml
git commit -m "Add CI workflow for build and test"
git push origin main
Before you check the result, what do you think will happen? Do you expect the workflow to pass or fail on this first run?
- Open your GitHub repository in your browser.
- Click the Actions tab in the repository header.
- Click on the most recent workflow run (it will be named after your commit message).
- Click the build job to expand the step details.
You should see a green checkmark next to every step, including Build and Test. In the Build and Test logs, you will see Maven downloading dependencies and running your MUnit tests with a BUILD SUCCESS message.
Seeing a red X on the workflow?
- Click into the failed job and read the error message. If you see a 401 or authentication error during dependency download, your MULE_EE_USERNAME or MULE_EE_PASSWORD secret may be incorrect. Re-enter them in GitHub Settings.
- If you see a YAML syntax error, check that your indentation uses spaces (not tabs) and that each step aligns at the same level.
- If the runner cannot find pom.xml, make sure your Mule project files are at the root of the repository (not inside a nested folder).
Help me debug my failed GitHub Actions workflow.
Your pipeline now builds and tests your Mule application on every push. Next up, you will deliberately break a test to see the pipeline catch it and block deployment.
Break the Build on Purpose
Your pipeline builds and tests on every push. The Actions tab shows a reassuring green checkmark.
A pipeline that only shows green is not proving its value. You need to see it catch a real failure so you understand what your team will experience when they push broken code.
In this step, get ready to:
- Write an MUnit test with an intentionally wrong assertion and push it to GitHub.
- Watch the GitHub Actions workflow fail and locate the MUnit error in the logs.
- Fix the assertion, push again, and confirm the pipeline goes green.
Create a broken MUnit test
An MUnit test invokes a Mule flow and asserts something about the result. You are going to write a test that asserts the wrong payload value on purpose.
This guarantees the pipeline will fail, proving it can catch broken code before deployment.
- In Anypoint Studio, create the directory src/test/munit inside your project if it does not already exist.
- Create a new file called greeting-test-suite.xml inside src/test/munit.
- Paste the following broken test into the file:
<mule xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:munit="http://www.mulesoft.org/schema/mule/munit" xmlns:munit-tools="http://www.mulesoft.org/schema/mule/munit-tools" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/munit http://www.mulesoft.org/schema/mule/munit/current/mule-munit.xsd http://www.mulesoft.org/schema/mule/munit-tools http://www.mulesoft.org/schema/mule/munit-tools/current/mule-munit-tools.xsd">
<munit:config name="greeting-test-suite" />
<munit:test name="test-greeting-response" description="Test greeting flow returns correct payload">
<munit:execution>
<flow-ref name="greetingFlow" />
</munit:execution>
<munit:validation>
<munit-tools:assert-that expression="#[payload]" is="#[MunitTools::equalTo('{ "message": "wrong" }')]" />
</munit:validation>
</munit:test>
</mule>
What does this test do?
The <flow-ref name="greetingFlow" /> line invokes your greeting flow as if a real HTTP request hit it. The assert-that line then checks the payload against a value you know is wrong.
Your flow returns { "message": "Hello from MuleSoft!" } but the assertion expects { "message": "wrong" }. This mismatch will cause the pipeline to fail.
- Save greeting-test-suite.xml.
- Commit and push the broken test to GitHub by running these commands in your terminal:
git add src/test/munit/greeting-test-suite.xml
git commit -m "Add broken MUnit test to prove pipeline catches failures"
git push origin main
Before you check the Actions tab, predict what you think will happen. Will the workflow pass or fail?
Watch the failure, then fix the test
- Open your GitHub repository in the browser.
- Click the Actions tab.
- You should see a workflow run with a red X icon next to it. Click into the failed run.
- Click the build job, then expand the Build and Test step in the log output.
In the logs, look for the MUnit Run Summary. You will see the test failed with a message indicating the expected value did not match the actual payload.
What should I see in the logs?
The MUnit output shows a failure line followed by an assertion error comparing the expected and actual values. The summary reports Failures: 1 and the build exits with BUILD FAILURE.
This proves your pipeline catches broken code before it can reach deployment.
The pipeline did exactly what it should. Now fix the assertion so it matches the actual payload your flow returns.
- In Anypoint Studio, open src/test/munit/greeting-test-suite.xml.
- Find this line:
<munit-tools:assert-that expression="#[payload]" is="#[MunitTools::equalTo('{ "message": "wrong" }')]" />
- Replace wrong with Hello from MuleSoft! so the line now looks like this:
<munit-tools:assert-that expression="#[payload]" is="#[MunitTools::equalTo('{ "message": "Hello from MuleSoft!" }')]" />
- Save greeting-test-suite.xml.
- Commit and push the fix by running:
git add src/test/munit/greeting-test-suite.xml
git commit -m "Fix MUnit assertion to match actual greeting payload"
git push origin main
Before you check the Actions tab again, predict the outcome. Will this run pass?
- Return to the Actions tab in GitHub. Wait for the new workflow run to complete.
- You should see a green checkmark. Click into the run and expand the Build and Test step to confirm the MUnit summary now shows Failures: 0.
Why break the build on purpose?
In an interview, you can explain that you deliberately introduced a failing test to validate the pipeline's safety net. A pipeline that has never caught a failure is an untested safety net.
Seeing it go red and then green gives you confidence it will protect a real codebase from regressions.
🙋♀️ Still seeing a red build after the fix?
Make sure the assertion string matches your flow's payload exactly, including spaces and curly braces. The comparison is literal.
Confirm you pushed to the main branch. The workflow only triggers on pushes to main.
Help me debug why my MUnit test is still failing after I fixed the assertion.
✔️ Awesome, I've got everything!
Great. Double check you have saved greeting-test-suite.xml and your latest push shows a green checkmark in the Actions tab.
ⓧ I'd like to double check the full code
Here is the final version of your test suite file:
<mule xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:munit="http://www.mulesoft.org/schema/mule/munit" xmlns:munit-tools="http://www.mulesoft.org/schema/mule/munit-tools" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/munit http://www.mulesoft.org/schema/mule/munit/current/mule-munit.xsd http://www.mulesoft.org/schema/mule/munit-tools http://www.mulesoft.org/schema/mule/munit-tools/current/mule-munit-tools.xsd">
<munit:config name="greeting-test-suite" />
<munit:test name="test-greeting-response" description="Test greeting flow returns correct payload">
<munit:execution>
<flow-ref name="greetingFlow" />
</munit:execution>
<munit:validation>
<munit-tools:assert-that expression="#[payload]" is="#[MunitTools::equalTo('{ "message": "Hello from MuleSoft!" }')]" />
</munit:validation>
</munit:test>
</mule>
You have proven your pipeline is a genuine safety net. It catches broken code and blocks it from progressing. Next up, you will close the full loop by adding a deploy stage that ships a passing build live to CloudHub 2.0.
Add the CD Stage (Deploy to CloudHub 2.0)
Your pipeline now builds and tests automatically on every push, but the app still lives only on your local machine.
In this step, you'll close that gap by adding a deployment stage to your workflow. A passing build will automatically deploy your Mule application to CloudHub 2.0, completing the full CI/CD cycle from code commit to live cloud endpoint.
In this step, get ready to:
- Configure the Mule Maven Plugin for CloudHub 2.0 deployment.
- Add API autodiscovery for future API Manager integration.
- Extend the GitHub Actions workflow with a deploy job.
Configure the Mule Maven Plugin for CloudHub 2.0
The Mule Maven Plugin handles packaging and deploying your Mule application. You need to configure it with the cloudhub2Deployment strategy so Maven knows how to push your app to the cloud.
- Open pom.xml in Anypoint Studio.
- Locate the <build><plugins> section (or create one if it does not exist yet).
- Replace (or add) the mule-maven-plugin configuration with the following:
<plugin>
<groupId>org.mule.tools.maven</groupId>
<artifactId>mule-maven-plugin</artifactId>
<version>4.2.0</version>
<extensions>true</extensions>
<configuration>
<cloudhub2Deployment>
<uri>https://anypoint.mulesoft.com</uri>
<provider>MC</provider>
<environment>${anypoint.environment}</environment>
<target>${anypoint.target}</target>
<applicationName>${anypoint.appName}</applicationName>
<muleVersion>${mule.version}</muleVersion>
<username>${anypoint.username}</username>
<password>${anypoint.password}</password>
<properties>
<anypoint.platform.client_id>${anypoint.platform.client_id}</anypoint.platform.client_id>
<anypoint.platform.client_secret>${anypoint.platform.client_secret}</anypoint.platform.client_secret>
<api.id>${api.id}</api.id>
</properties>
</cloudhub2Deployment>
</configuration>
</plugin>
What does this configuration do?
Every value in ${} is a Maven property placeholder. The CI pipeline injects the real values via -D flags at build time, so no credentials live in your source code.
- provider: MC tells the plugin to use the MuleSoft Control Plane (CloudHub 2.0).
- The <properties> block passes runtime properties to the deployed application, including the environment credentials for API autodiscovery and the API instance ID.
- Save pom.xml.
Next, you need to add the autodiscovery element so API Manager can recognize this application later.
- Open src/main/mule/greeting.xml in Anypoint Studio.
- Update the root <mule> element to add the api-gateway namespace, and add the autodiscovery element before the <flow> tag. Replace the entire file content with:
<mule xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:http="http://www.mulesoft.org/schema/mule/http" xmlns:api-gateway="http://www.mulesoft.org/schema/mule/api-gateway" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd http://www.mulesoft.org/schema/mule/api-gateway http://www.mulesoft.org/schema/mule/api-gateway/current/mule-api-gateway.xsd">
<http:listener-config name="HTTP_Listener_config" doc:name="HTTP Listener config">
<http:listener-connection host="0.0.0.0" port="8081" />
</http:listener-config>
<api-gateway:autodiscovery apiId="${api.id}" flowRef="greetingFlow" doc:name="Autodiscovery" />
<flow name="greetingFlow">
<http:listener doc:name="Listener" config-ref="HTTP_Listener_config" path="/greeting" />
<set-payload value='{ "message": "Hello from MuleSoft!" }' mimeType="application/json" doc:name="Set Payload" />
</flow>
</mule>
What is autodiscovery?
The api-gateway:autodiscovery element pairs your running application with an API instance in API Manager. Once paired, any policies you apply in API Manager (like rate limiting) are automatically enforced by the embedded Mule Gateway at runtime.
The API Autodiscovery and API Gateway modules are Enterprise-only features. Project should include configuration to access enterprise repository.
The apiId uses ${api.id} so it can be injected from the pipeline. You'll set this to a real value in the next step when the pipeline creates the API instance.
- Save greeting.xml.
- Run the application locally in Anypoint Studio to confirm it still starts without errors.
The app should start and still respond to requests at http://localhost:8081/greeting. The autodiscovery element logs a warning about the API ID being 0 or invalid. That is expected for now.
Seeing a startup error?
- Make sure you added the xmlns:api-gateway namespace AND its schemaLocation entry in the root <mule> tag.
- Check that flowRef="greetingFlow" matches your flow's name attribute exactly.
Help me debug my autodiscovery configuration error
Add GitHub Secrets and extend the workflow
The deploy job needs Anypoint Platform credentials to push your application to CloudHub 2.0. It also needs environment client credentials so the deployed runtime can connect to API Manager for autodiscovery.
- Open your GitHub repository in a browser.
- Click **Settings** in the repository navigation bar.
- In the left sidebar under **Security**, expand **Secrets and variables**, then click **Actions**.
- Click the **Secrets** tab, then click **New repository secret**.
- Add a secret with the name ANYPOINT_USERNAME and paste your Anypoint Platform username as the value.
- Click **Add secret**.
- Repeat to create ANYPOINT_PASSWORD with your Anypoint Platform password.
Now you need the environment client credentials for autodiscovery.
- Open Anypoint Platform in a new tab and log in.
- Click **Access Management** from the gear icon menu.
- In the left navigation, click **Business Groups**.
- Click the name of your root organization.
- Click the **Environments** tab.
- Click **Sandbox** to view its client ID and client secret.
- Back in GitHub, create a secret named ENV_CLIENT_ID with the Sandbox environment client ID.
- Create a secret named ENV_CLIENT_SECRET with the Sandbox environment client secret.
You should now have four new secrets (ANYPOINT_USERNAME, ANYPOINT_PASSWORD, ENV_CLIENT_ID, ENV_CLIENT_SECRET) alongside the two from earlier.
Why environment credentials?
The Mule runtime uses anypoint.platform.client_id and anypoint.platform.client_secret to authenticate with API Manager at startup. Without these, the autodiscovery element cannot pair the running app with its API instance, and policies would not be enforced.
Now update the workflow file to add the deploy job.
- Open .github/workflows/build.yml in your editor.
- Find the workflow name on line 1:
name: CI Pipeline
- Change it to:
name: CI/CD Pipeline
- Find the job name build: and rename it to build-and-test: so it accurately reflects what the job does.
- Below the closing of the build-and-test job (after its last step), add the deploy job declaration by pasting this code:
deploy:
runs-on: ubuntu-latest
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
Why repeat checkout and Java setup?
Each GitHub Actions job runs on a fresh virtual machine. The deploy job needs to check out the code and set up Java independently because it does not share a filesystem with the build-and-test job.
needs: build-and-test ensures the deploy job only runs after tests pass.
- Below the Java setup step in the deploy job, add the Maven settings.xml generation step (identical to the one in the build-and-test job):
- name: Generate Maven settings.xml
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings>
<servers>
<server>
<id>MuleRepository</id>
<username>${{ secrets.MULE_EE_USERNAME }}</username>
<password>${{ secrets.MULE_EE_PASSWORD }}</password>
</server>
</servers>
<profiles>
<profile>
<id>mule-extra-repos</id>
<activation><activeByDefault>true</activeByDefault></activation>
<repositories>
<repository>
<id>MuleRepository</id>
<url>https://repository.mulesoft.org/releases/</url>
</repository>
</repositories>
</profile>
</profiles>
</settings>
EOF
- Finally, add the Deploy to CloudHub 2.0 step that runs the Mule Maven Plugin with all the deployment properties:
- name: Deploy to CloudHub 2.0
run: |
mvn clean deploy -DmuleDeploy \
-Danypoint.username=${{ secrets.ANYPOINT_USERNAME }} \
-Danypoint.password=${{ secrets.ANYPOINT_PASSWORD }} \
-Danypoint.environment=Sandbox \
-Danypoint.target=Cloudhub-US-East-2 \
-Danypoint.appName=greeting-api \
-Dmule.version=4.6.0 \
-Danypoint.platform.client_id=${{ secrets.ENV_CLIENT_ID }} \
-Danypoint.platform.client_secret=${{ secrets.ENV_CLIENT_SECRET }} \
-Dapi.id=0
What do these flags do?
- -DmuleDeploy tells the Mule Maven Plugin to execute the deployment configuration in your pom.xml.
- -Danypoint.target=Cloudhub-US-East-2 specifies the CloudHub 2.0 shared space target.
- -Dapi.id=0 is a temporary placeholder. The next step replaces this with a real API instance ID after the pipeline creates one in API Manager.
- Save build.yml.
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files (pom.xml, greeting.xml, build.yml).
ⓧ I'd like to double check the full code
Scroll up and confirm you have each of the following in your build.yml, in order:
- Workflow name set to CI/CD Pipeline.
- The build-and-test job (renamed from build) with Checkout, Java setup, settings.xml generation, and mvn clean test.
- The deploy job with needs: build-and-test, its own Checkout, Java setup, settings.xml generation, and the Deploy to CloudHub 2.0 step.
For pom.xml, confirm the mule-maven-plugin has version 4.2.0, extensions set to true, and the full cloudhub2Deployment block with property placeholders.
For greeting.xml, confirm the api-gateway namespace is declared and the <api-gateway:autodiscovery> element sits before the <flow> tag.
Push and verify the full pipeline
Time to see the complete CI/CD cycle in action.
- Commit all your changes and push to the main branch by running these commands:
git add .
git commit -m "Add CD stage: deploy to CloudHub 2.0"
git push origin main
- Open your GitHub repository in a browser and click the **Actions** tab.
Before you click into the running workflow, predict what you expect to see. Will the deploy job run immediately, or will it wait for something first?
- Click the running workflow to watch it.
You should see two jobs: build-and-test runs first, and once it completes with a green checkmark, deploy begins. The deploy job takes several minutes as it uploads and starts your application on CloudHub 2.0.
Deployment takes 3-5 minutes
CloudHub 2.0 needs to provision a container and start the Mule runtime. This is normal for the first deployment. Subsequent deploys are faster because the container already exists.
- Once the workflow shows both jobs green, open Anypoint Platform.
- Navigate to **Runtime Manager** from the left navigation menu.
- Make sure you are viewing the **Sandbox** environment (use the environment switcher at the top).
- Confirm that your greeting-api application appears with a **Started** status.
- Click the application name to view its details and copy the public URL shown in Runtime Manager.
The URL follows the pattern greeting-api-<unique-id>.<shard>.<region>.cloudhub.io. CloudHub 2.0 appends a unique identifier to prevent domain conflicts.
- Test the live endpoint by sending a GET request to your CloudHub URL with the /greeting path appended. You can use your browser or curl:
curl https://[[CLOUDHUB_URL="your-app-url.shard.region.cloudhub.io"]]/greeting
You should receive the greeting JSON response:
{ "message": "Hello from MuleSoft!" }
Your Mule application is now live on CloudHub 2.0, deployed automatically by your pipeline.
Deploy job failed?
- Click into the failed step in the Actions log and read the error message.
- If you see an authentication error, verify your ANYPOINT_USERNAME and ANYPOINT_PASSWORD secrets match your Anypoint Platform login credentials exactly.
- If the target is not found, confirm your Sandbox environment has a shared space available. Trial accounts include a default shared space.
Help me troubleshoot my CloudHub 2.0 deployment failure in GitHub Actions
Your pipeline now handles the full journey from code push to live cloud deployment. Next up, you'll make the pipeline even smarter by having it automatically register the API in API Manager and apply a rate limiting policy.
Automate API Manager Instance and Rate Limiting Policy
Your CI/CD pipeline builds, tests, and deploys automatically. But the deployed API is completely unprotected and invisible to API Manager.
A production-ready pipeline should also provision the governance layer programmatically. In this step, you will extend your workflow to create an API instance in API Manager and apply a Rate Limiting policy, all without a single manual click.
In this step, get ready to:
- Add your Anypoint organization and environment IDs as GitHub Secrets.
- Create a new workflow job that provisions an API instance and applies a rate limiting policy via the Anypoint Platform REST API.
- Verify that rapid requests to your endpoint return HTTP 429 after exceeding the quota.
Store your organization and environment IDs as secrets
The API Manager REST API requires your Anypoint Platform organization ID and environment ID in every URL. You need to retrieve these values and store them securely in GitHub.
- Sign in to Anypoint Platform.
- Click the gear icon in the top navigation and select Access Management.
- In the left sidebar, click Business Groups.
- Click your root organization name.
- Look at your browser's address bar. The URL ends with /businessGroups/ followed by a UUID. Copy that UUID. This is your organization ID.
Now retrieve your Sandbox environment ID.
- While still in Access Management, click the Environments tab.
- Click Sandbox.
- Look at the browser address bar again. The URL now contains your environment ID (UUID) after the environment path segment. Copy it.
What are these IDs?
Your organization ID identifies your Anypoint Platform tenant. Your environment ID identifies the specific runtime context (Sandbox, Production, etc.) where APIs are deployed and managed.
The API Manager REST API needs both to know exactly which API registry to target when creating instances and policies.
- In your GitHub repository, navigate to Settings then Secrets and variables then Actions.
- Click New repository secret and add a secret named ANYPOINT_ORG_ID with your organization UUID as the value.
- Add another secret named ANYPOINT_ENV_ID with your Sandbox environment ID as the value.
Can't find your organization ID?
Make sure you clicked the root business group name (not a child group). The UUID appears in the URL immediately after /businessGroups/. If the URL does not change, try the classic UI path: Access Management then Organization tab. The ID appears in the Business Group Id field.
Help me find my organization and environment IDs
Add the API governance job to your workflow
You will add a new job called provision-api between the build-and-test and deploy jobs. This job obtains a bearer token, creates an API instance in API Manager, and applies a Rate Limiting policy.
- Create payload template file under .github/workflows/create-api-instance-payload-template.json this payload can be inserted into REST request in build.yml. This will keep the file clean instead of writing inline payload.
{
"technology": "mule4",
"endpoint": {
"uri": "$API_URI",
"proxyUri": null,
"isCloudHub": false
},
"instanceLabel": "$API_LABEL",
"spec": {
"assetId": "$API_ASSET_ID",
"groupId": "$API_GROUP_ID",
"version": "$API_ASSET_VERSION"
}
}
- Open .github/workflows/build.yml in your editor.
- Find the blank line between the end of the build-and-test job and the start of the deploy job.
- Add the following provision-api job with its first two steps (Get Bearer Token and Create API Instance):
provision-api:
runs-on: ubuntu-latest
needs: build-and-test
outputs:
api_id: ${{ steps.create-api.outputs.api_id }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Bearer Token
id: get-token
run: |
TOKEN=$(curl -s -X POST https://anypoint.mulesoft.com/accounts/api/v2/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "client_id=${{ secrets.CONNECTED_APP_CLIENT_ID }}" \
--data-urlencode "client_secret=${{ secrets.CONNECTED_APP_CLIENT_SECRET }}" \
--data-urlencode "grant_type=client_credentials" \
| jq -r '.access_token')
echo "token=$TOKEN" >> $GITHUB_OUTPUT
- name: Create API Instance
id: create-api
env:
API_URI: "https://greeting-api.cloudhub.io"
API_LABEL: "greeting-api-instance-v1"
API_GROUP_ID: "<org-id/ business group id where api instance to created>"
API_ASSET_ID: "test-instance"
API_ASSET_VERSION: "<your asset version>"
run: |
# 1. Substitute variables in the template and output to a temporary payload file
envsubst < .github/workflows/create-api-instance-payload-template.json > temp-api-instance-payload.json
# 2. (Optional debug step) Echo the compiled payload to verify values are set
# echo "Generated Payload:"
# cat temp-api-instance-payload.json
# 3. Post the compiled temporary JSON file directly
API_ID=$(curl -s -X POST \
"https://anypoint.mulesoft.com/apimanager/api/v1/organizations/${{ secrets.ANYPOINT_ORG_ID }}/environments/${{ secrets.ANYPOINT_ENV_ID }}/apis" \
-H "Authorization: Bearer ${{ steps.get-token.outputs.token }}" \
-H "Content-Type: application/json" \
-d "@temp-api-instance-payload.json" | jq -r '.id')
echo "api_id=$API_ID" >> $GITHUB_OUTPUT
What does this code do?
- The outputs block exposes the API Instance ID so the deploy job can read it later.
- Get Bearer Token calls the Anypoint Platform OAuth2 token endpoint with your credentials. It stores the token as a step output using $GITHUB_OUTPUT.
- Create API Instance POSTs to the API Manager REST API. The JSON body specifies mule4 as the technology, your CloudHub endpoint URI, and a label for the instance. It extracts the returned id field using jq.
- Add the third step that applies the Rate Limiting policy directly below the Create API Instance step, still inside the provision-api job:
- name: Apply Rate Limiting Policy
run: |
curl -s -X POST \
"https://anypoint.mulesoft.com/apimanager/api/v1/organizations/${{ secrets.ANYPOINT_ORG_ID }}/environments/${{ secrets.ANYPOINT_ENV_ID }}/apis/${{ steps.create-api.outputs.api_id }}/policies" \
-H "Authorization: Bearer ${{ steps.get-token.outputs.token }}" \
-H "Content-Type: application/json" \
-d '{
"groupId": "68ef9520-24e9-4cf2-b2f5-620025690913",
"assetId": "rate-limiting",
"assetVersion": "1.3.3",
"configuration": {
"rateLimits": [{ "maximumRequests": 3, "timePeriodInMilliseconds": 60000 }],
"clusterizable": true,
"exposeHeaders": false
}
}'
What does this code do?
This step POSTs to the /policies endpoint for the newly created API instance. The body identifies the built-in Rate Limiting policy by its Exchange GAV (GroupId, AssetId, AssetVersion) and configures it to allow 3 requests per 60,000 milliseconds (1 minute). Any request beyond that quota receives HTTP 429.
The groupId value 68ef9520-24e9-4cf2-b2f5-620025690913 is the MuleSoft organization that publishes the built-in policies on Exchange. You do not need to change this.
Finally, update the deploy job so it runs after provision-api and receives the API Instance ID.
- In the deploy job, find this line:
needs: build-and-test
- Replace it with:
needs: provision-api
- In the same deploy job, find this line at the end of the Maven deploy command:
-Dapi.id=0
- Replace it with:
-Dapi.id=${{ needs.provision-api.outputs.api_id }}
- Save .github/workflows/build.yml.
Why wire up the api_id output?
The api-gateway:autodiscovery element in your Mule app uses ${api.id} to pair the running application with its API Manager instance. By passing the real API Instance ID from the provision-api job into the deploy, the Mule runtime connects to API Manager on startup and starts enforcing the Rate Limiting policy.
✔️ Awesome, I've got everything!
Great. Double check you saved .github/workflows/build.yml before continuing.
ⓧ I'd like to double check the full code
Your build.yml is now quite long. Scroll through and confirm you have these three jobs in order:
- The build-and-test job (unchanged from before).
- The provision-api job with needs: build-and-test, an outputs block, and three steps: Get Bearer Token, Create API Instance, Apply Rate Limiting Policy.
- The deploy job with needs: provision-api and the last Maven flag reading -Dapi.id=${{ needs.provision-api.outputs.api_id }}.
Push and verify the rate limiting policy
- Commit and push your changes by running these commands:
git add .github/workflows/build.yml
git commit -m "Add API governance job with rate limiting"
git push origin main
- Open your GitHub repository in the browser and click the Actions tab.
- Watch the pipeline run. You should see three jobs in sequence: build-and-test, provision-api, and deploy.
- Wait for all three jobs to show green checkmarks.
Seeing a failure in provision-api?
If the Get Bearer Token step fails, verify that ANYPOINT_USERNAME and ANYPOINT_PASSWORD are set correctly in your GitHub Secrets. If the Create API Instance step fails, check that ANYPOINT_ORG_ID and ANYPOINT_ENV_ID contain the correct UUIDs with no extra spaces or quotes.
Help me debug my provision-api job
- Once the pipeline completes, open Anypoint Platform and navigate to API Manager.
- Select the Sandbox environment from the environment dropdown.
- Confirm you see an API instance labelled greeting-api-instance with status Active.
Now test the rate limit. Before you run the next command, predict what will happen on the fourth request. The policy allows 3 requests per minute.
- Send 4 rapid GET requests to your CloudHub endpoint by running:
for i in 1 2 3 4; do
echo "Request $i:"
curl -s -o /dev/null -w "%{http_code}\n" https://[[CLOUDHUB_URL="your CloudHub greeting-api URL"]]/greeting
done
Requests 1 through 3 return 200. The fourth request returns 429. That is the Rate Limiting policy rejecting the request because you exceeded the 3-per-minute quota.
All four requests return 200?
The policy takes a moment to propagate after the Mule runtime pairs with API Manager via autodiscovery. Wait 30 seconds and try the loop again. If it still returns 200 for all requests, verify in API Manager that the Rate Limiting policy appears under the Policies tab of your API instance.
Help me troubleshoot my Rate Limiting policy
Your pipeline now handles the full lifecycle: test, govern, and deploy. Next up, you will push the boundaries with a Secret Mission that replaces username/password authentication with a Connected App for production-grade security.
Secret mission
Upgrade to Connected App Authentication
Your pipeline works, but it authenticates with a username and password. In production, that means a human account's credentials sitting in your CI secrets. Connected Apps use OAuth 2.0 client_credentials, scoped to exactly what the pipeline needs. Swap out the credentials and bring your pipeline up to production security standards.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your CloudHub 2.0 application consumes vCore resources from your Anypoint Platform allocation while running.
Cost warning
Your Mule application deployed to CloudHub 2.0 consumes Sandbox vCores while it is running. If you are on a trial account, your 30-day trial will eventually expire. If you are on a paid account, consider stopping or deleting the application to free up vCore allocation.
Resources you used:
- Mule application deployed on CloudHub 2.0 (Sandbox environment).
- API instance in API Manager with a Rate Limiting policy applied.
- GitHub repository with GitHub Actions workflow runs.
- GitHub Secrets storing credentials for the pipeline.
- Local Mule project files on your computer.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the pipeline.
- Your CloudHub 2.0 app remains deployed and accessible at its public URL.
- Your API Manager instance continues to enforce the Rate Limiting policy.
- GitHub Actions will continue to run on every push to the main branch.
- The application consumes Sandbox vCores while running.
Pause - I'll come back to this later
Stop the application to free up vCore resources while keeping all configuration intact. You can restart it later without redeploying.
- Open Anypoint Platform and navigate to Runtime Manager.
- In the Applications tab, click your application name to open its management panel.
- Click the down-arrow next to the application status.
- Click Stop.
- The app no longer consumes vCores but remains configured for a quick restart.
Delete - I don't want to use this again
Remove all project resources completely and free up all allocations.
Delete the CloudHub 2.0 application:
- Open Anypoint Platform and navigate to Runtime Manager.
- In the Applications tab, click your application name to open its management panel.
- Click the down-arrow next to the application status and select Delete.
Delete the API Manager instance:
- Navigate to Anypoint Platform > API Manager.
- In API Administration, click the name of your API instance.
- Select Actions > Delete API.
- Click Delete to confirm.
Delete the Connected App:
- Navigate to Access Management > Connected Apps.
- In Owned Apps, locate your Connected App.
- Click the more actions menu and select Remove. Confirm the deletion.
Delete the GitHub repository:
- Open your repository on GitHub.
- Click Settings (under the repository name).
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository, then I have read and understand these effects.
- Type the repository name and click Delete this repository.
Delete local project files:
- Delete the local project folder from your computer using your file manager or by running the appropriate remove command in your terminal.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully automated CI/CD pipeline that takes a MuleSoft API from code commit to live deployment on CloudHub 2.0 with zero manual intervention.
You've learned how to:
- Build a CI/CD pipeline from scratch using GitHub Actions and the Mule Maven Plugin that automatically runs MUnit tests, packages, and deploys your Mule 4 application on every push to main.
- Experience a pipeline catching a broken MUnit test and blocking deployment, then fix the test and watch it go green.
- Automate API Manager governance programmatically by using the Anypoint Platform REST API to create an API instance and apply a Rate Limiting policy that returns HTTP 429 when the quota is exceeded.
- Secret Mission: Replace basic credentials with a Connected App using OAuth 2.0 client_credentials grant for production-grade CI/CD security.
Ready to quiz yourself?