Setting Up a CI/CD Pipeline for Power Apps Code Apps with Azure DevOps
Power Apps Code Apps let you build React (or Vue, or whatever you prefer) apps that run inside Power Platform with full access to Dataverse, connectors, and enterprise auth. It is a legitimate code-first development experience.
The deployment story starts manual. You run pac code push from your machine, it uploads to your environment, done. That works for one person on one machine. It stops working the moment you have a team, multiple environments, or any expectation that main reflects what is actually running in production.
This post walks through setting up an Azure Pipelines CI/CD pipeline that deploys your Code App automatically when code merges to main in Azure Repos.
What You Need Before Starting
- A Power Apps Code App project committed to an Azure Repos Git repository
- A Power Platform environment with Code Apps enabled
- An Azure AD app registration (service principal) with access to your Power Platform environments
- An Azure DevOps organization with the Microsoft Power Platform Build Tools extension installed
If Code Apps are not yet enabled on your environment, go to the Power Platform admin center, select your environment, then Settings > Product > Features, and toggle on Power Apps code apps.
For the Build Tools extension, go to the Visual Studio Marketplace and install it into your Azure DevOps organization. It is free.
Step 1: Create an Azure AD App Registration
The pipeline authenticates against Power Platform using a service principal. Do not use personal credentials here.
- Go to Azure Active Directory > App registrations > New registration
- Give it a meaningful name, like
my-codeapp-cicd - Under Certificates & secrets, create a new client secret. Copy the value immediately.
- Note down three things:
- Application (client) ID
- Directory (tenant) ID
- Client secret value
Next, register this service principal as an Application User in each Power Platform environment you want to deploy to.
- Go to the Power Platform admin center
- Select your target environment, then Settings > Users + permissions > Application users
- Click New app user, select your app registration, assign the System Administrator security role, and save
Repeat this for every environment (dev, staging, production) that the pipeline will touch.
Step 2: Create a Service Connection in Azure DevOps
Azure DevOps connects to Power Platform through a Service Connection. This is where you store the credentials, not in pipeline variables.
- In your Azure DevOps project, go to Project settings > Pipelines > Service connections
- Click New service connection and select Power Platform
- Fill in the following:
- Server URL: your Power Platform environment URL, e.g.
https://yourorg.crm11.dynamics.com - Tenant ID: Directory (tenant) ID from your app registration
- Application ID: Application (client) ID
- Client secret: the client secret value
- Server URL: your Power Platform environment URL, e.g.
- Give it a recognisable name, like
PowerPlatform-Production - Save it
If you are deploying to multiple environments, create a separate service connection for each one. You will reference them by name in the pipeline YAML.
Step 3: Store Pipeline Variables
Credentials live in the Service Connection. Other pipeline configuration goes in a Variable Group.
- In Azure DevOps, go to Pipelines > Library > + Variable group
- Create a group called
codeapp-deploy - Add any variables your pipeline needs, for example the app display name or environment URLs if you want them in one place
- Mark secrets as secret (the padlock icon) so they are masked in logs
You can also define variables directly in the pipeline YAML for non-secret values. Variable Groups are most useful when you share variables across multiple pipelines.
Step 4: Write the Pipeline YAML
In your Azure Repos repository, create azure-pipelines.yml at the root:
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
variables:
- group: codeapp-deploy
- name: NODE_VERSION
value: '20'
stages:
- stage: Deploy
displayName: Deploy to Power Platform
jobs:
- job: DeployJob
displayName: Build and deploy code app
steps:
- task: NodeTool@0
displayName: Setup Node.js
inputs:
versionSpec: $(NODE_VERSION)
- script: npm ci
displayName: Install dependencies
- script: npm test --if-present
displayName: Run tests
- script: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
displayName: Install PAC CLI
- script: echo "##vso[task.prependpath]$HOME/.dotnet/tools"
displayName: Add PAC CLI to PATH
- script: |
pac auth create \
--url $(PP_ENVIRONMENT_URL) \
--applicationId $(PP_CLIENT_ID) \
--clientSecret $(PP_CLIENT_SECRET) \
--tenant $(PP_TENANT_ID)
displayName: Authenticate with Power Platform
env:
PP_ENVIRONMENT_URL: $(PP_ENVIRONMENT_URL)
PP_CLIENT_ID: $(PP_CLIENT_ID)
PP_CLIENT_SECRET: $(PP_CLIENT_SECRET)
PP_TENANT_ID: $(PP_TENANT_ID)
- script: pac code push
displayName: Deploy code app
The echo "##vso[task.prependpath]..." line is an Azure Pipelines logging command that adds the dotnet tools directory to the agent’s PATH for all subsequent steps. Without it, the pac command will not be found even though it installed successfully.
Step 5: Hook the Pipeline to Your Repository
- In Azure DevOps, go to Pipelines > New pipeline
- Select Azure Repos Git as the source
- Select your repository
- Choose Existing Azure Pipelines YAML file and point it at
azure-pipelines.yml - Save and run
The pipeline will trigger automatically on every push to main from this point on.
Step 6: Multi-Environment Deployments with Approval Gates
A single-stage pipeline that deploys straight to production is fine when you are the only person working on the project. Once there is a team, you want dev to deploy automatically and production to require a manual approval.
Azure DevOps handles this with Environments and pre-deployment approvals.
First, create your environments:
- Go to Pipelines > Environments > New environment
- Create one called
Developmentand one calledProduction - On the
Productionenvironment, click the three-dot menu, select Approvals and checks, and add an Approval with the relevant reviewers
Then update your pipeline to use two stages with separate service connections:
trigger:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
variables:
- name: NODE_VERSION
value: '20'
stages:
- stage: DeployDev
displayName: Deploy to Development
jobs:
- deployment: DeployDev
displayName: Deploy to dev environment
environment: Development
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: NodeTool@0
inputs:
versionSpec: $(NODE_VERSION)
- script: npm ci
displayName: Install dependencies
- script: npm test --if-present
displayName: Run tests
- script: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
displayName: Install PAC CLI
- script: echo "##vso[task.prependpath]$HOME/.dotnet/tools"
displayName: Add PAC CLI to PATH
- task: PowerPlatformToolInstaller@2
displayName: Install Power Platform Build Tools
- script: |
pac auth create \
--url $(PP_DEV_URL) \
--applicationId $(PP_CLIENT_ID) \
--clientSecret $(PP_CLIENT_SECRET) \
--tenant $(PP_TENANT_ID)
pac code push
displayName: Deploy to development
env:
PP_DEV_URL: $(PP_DEV_URL)
PP_CLIENT_ID: $(PP_CLIENT_ID)
PP_CLIENT_SECRET: $(PP_CLIENT_SECRET)
PP_TENANT_ID: $(PP_TENANT_ID)
- stage: DeployProd
displayName: Deploy to Production
dependsOn: DeployDev
condition: succeeded()
jobs:
- deployment: DeployProd
displayName: Deploy to production environment
environment: Production
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: NodeTool@0
inputs:
versionSpec: $(NODE_VERSION)
- script: npm ci
displayName: Install dependencies
- script: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
displayName: Install PAC CLI
- script: echo "##vso[task.prependpath]$HOME/.dotnet/tools"
displayName: Add PAC CLI to PATH
- script: |
pac auth create \
--url $(PP_PROD_URL) \
--applicationId $(PP_CLIENT_ID) \
--clientSecret $(PP_CLIENT_SECRET) \
--tenant $(PP_TENANT_ID)
pac code push
displayName: Deploy to production
env:
PP_PROD_URL: $(PP_PROD_URL)
PP_CLIENT_ID: $(PP_CLIENT_ID)
PP_CLIENT_SECRET: $(PP_CLIENT_SECRET)
PP_TENANT_ID: $(PP_TENANT_ID)
The deployment job type (rather than a regular job) is what enables the Environment integration. When the pipeline reaches the Production stage, it pauses and sends an approval notification to whoever you configured. Nothing deploys to production until someone clicks approve.
The dependsOn: DeployDev and condition: succeeded() ensure the production stage only runs if the dev deployment completed without errors.
Step 7: Store Secrets Securely
Never put credentials directly in YAML. Two options:
Variable Groups with Azure Key Vault (recommended for teams): Link your Variable Group to an Azure Key Vault secret store. Your credentials live in Key Vault, the Variable Group pulls them at runtime, and you get audit logs and rotation without touching the pipeline.
Pipeline secret variables: In the pipeline editor, go to Variables and add each secret with the lock icon enabled. These are masked in logs and not stored in the YAML file. Reference them with $(VARIABLE_NAME) syntax.
Either approach keeps credentials out of the repository.
What Can Go Wrong
pac command not found. The dotnet global tools path is not automatically on the agent’s PATH. The ##vso[task.prependpath] step handles this. If you are still seeing the error, check that the install step ran before the command step.
Authentication fails with No profiles were found. The pac auth create step must run before pac code push in the same job. Profiles are not shared between jobs. If you split these across jobs, you will need to re-authenticate in each one.
The app deploys but shows stale content. Code Apps cache aggressively in the browser. Hard refresh (Ctrl+Shift+R) or clear the app cache. If the pipeline logs show a successful push, the updated code is on the server.
The production stage never triggers. Check that the DeployDev stage actually completed with a Succeeded result, not SucceededWithIssues. The condition: succeeded() only passes on a clean success.
Pipeline parallelism error on first run. New Azure DevOps organisations have zero parallel jobs by default for public projects and need a free parallelism grant from Microsoft. Request one at https://aka.ms/azpipelines-parallelism-request or add a self-hosted agent.
A Note on the New npm CLI
The Power Apps Code Apps docs mention that @microsoft/power-apps v1.0.4 and higher includes its own npm-based CLI that will eventually replace pac code commands. At time of writing, pac code push is still the documented deployment path. Watch the Code Apps release notes if you are starting a project today, as the CLI tooling is actively changing and the pipeline setup above may simplify once the npm CLI is stable.