Reference patterns for generating CI/CD pipelines for Power Pages deployments. Used by the setup-pipeline skill.
| Platform | Skill Status | Reference Patterns |
|---|---|---|
| Power Platform Pipelines | ✅ Fully implemented | Section 4 below |
| Azure DevOps | 🚧 Coming soon | Section 2 below |
| GitHub Actions | 🚧 Coming soon | Section 3 below |
Full ADO/GitHub implementation spec:
C:\Users\nityagi\OneDrive - Microsoft\Design Documents\Plans\ALM skills for plugin\ado-cicd-skills-guide.md
Both ADO and GitHub Actions pipelines authenticate PAC CLI using a service principal (app registration).
- Create an app registration in Entra ID (Azure AD)
- Add the app as a System Administrator or Power Pages Site Owner in each target environment (Power Platform Admin Center → Environments → Settings → Users → App Users)
- Store credentials as secrets in ADO or GitHub
pac auth create \
--applicationId "$APP_ID" \
--clientSecret "$CLIENT_SECRET" \
--tenant "$TENANT_ID" \
--environment "$ENV_URL" \
--name "pipeline-auth"For certificate-based auth (more secure, recommended for production):
pac auth create \
--applicationId "$APP_ID" \
--certificateThumbprint "$CERT_THUMBPRINT" \
--tenant "$TENANT_ID" \
--environment "$ENV_URL"pac pages upload-code-site --rootPath "."This command uploads the compiled site from the compiledPath defined in powerpages.config.json. Always run npm run build before this step.
⚠️ Coming Soon — Thesetup-pipelineGitHub/ADO path is not yet implemented.
# azure-pipelines.yml
# Power Pages CI/CD Pipeline
# Requires pipeline variables: APP_ID, CLIENT_SECRET, TENANT_ID
# Requires environment-specific variables: DEV_ENV_URL, STAGING_ENV_URL, PROD_ENV_URL
trigger:
branches:
include:
- main
- release/*
pr:
branches:
include:
- main
variables:
nodeVersion: '20.x'
# Solution export/import variables (uncomment if using solution-based deployment)
# SOLUTION_NAME: 'ContosoSite'
stages:
# ─── Build ────────────────────────────────────────────────────────────────
- stage: Build
displayName: 'Build'
jobs:
- job: BuildSite
displayName: 'Build Power Pages Site'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '$(nodeVersion)'
displayName: 'Install Node.js'
- script: npm ci
displayName: 'Install dependencies'
- script: npm run build
displayName: 'Build site'
- task: PublishPipelineArtifact@1
inputs:
targetPath: 'dist'
artifact: 'site-build'
displayName: 'Publish build artifact'
# ─── Deploy to Dev ─────────────────────────────────────────────────────────
- stage: DeployDev
displayName: 'Deploy to Dev'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployToDev
displayName: 'Deploy to Dev Environment'
environment: 'dev'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: 'site-build'
path: 'dist'
- script: |
dotnet tool install --global Microsoft.PowerApps.CLI.Tool 2>/dev/null || true
pac auth create \
--applicationId "$(APP_ID)" \
--clientSecret "$(CLIENT_SECRET)" \
--tenant "$(TENANT_ID)" \
--environment "$(DEV_ENV_URL)"
pac pages upload-code-site --rootPath "."
displayName: 'Deploy to Dev'
env:
APP_ID: $(APP_ID)
CLIENT_SECRET: $(CLIENT_SECRET)
TENANT_ID: $(TENANT_ID)
DEV_ENV_URL: $(DEV_ENV_URL)
# ─── Deploy to Staging ─────────────────────────────────────────────────────
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: DeployDev
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToStaging
displayName: 'Deploy to Staging Environment'
environment: 'staging'
# Add an approval check in ADO: Environments → staging → Approvals and checks
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: 'site-build'
path: 'dist'
- script: |
dotnet tool install --global Microsoft.PowerApps.CLI.Tool 2>/dev/null || true
pac auth create \
--applicationId "$(APP_ID)" \
--clientSecret "$(CLIENT_SECRET)" \
--tenant "$(TENANT_ID)" \
--environment "$(STAGING_ENV_URL)"
pac pages upload-code-site --rootPath "."
displayName: 'Deploy to Staging'
env:
APP_ID: $(APP_ID)
CLIENT_SECRET: $(CLIENT_SECRET)
TENANT_ID: $(TENANT_ID)
STAGING_ENV_URL: $(STAGING_ENV_URL)
# ─── Deploy to Production ──────────────────────────────────────────────────
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProduction
displayName: 'Deploy to Production Environment'
environment: 'production'
# Add an approval check in ADO: Environments → production → Approvals and checks
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: DownloadPipelineArtifact@2
inputs:
artifact: 'site-build'
path: 'dist'
- script: |
dotnet tool install --global Microsoft.PowerApps.CLI.Tool 2>/dev/null || true
pac auth create \
--applicationId "$(APP_ID)" \
--clientSecret "$(CLIENT_SECRET)" \
--tenant "$(TENANT_ID)" \
--environment "$(PROD_ENV_URL)"
pac pages upload-code-site --rootPath "."
displayName: 'Deploy to Production'
env:
APP_ID: $(APP_ID)
CLIENT_SECRET: $(CLIENT_SECRET)
TENANT_ID: $(TENANT_ID)
PROD_ENV_URL: $(PROD_ENV_URL)
# ─── Solution Export/Import (uncomment to enable solution-based deployment) ──
# Add these stages between Build and DeployDev if using Dataverse solutions:
#
# - stage: ExportSolution
# dependsOn: Build
# jobs:
# - job: ExportSolution
# steps:
# - script: |
# pac auth create --applicationId "$(APP_ID)" --clientSecret "$(CLIENT_SECRET)" --tenant "$(TENANT_ID)" --environment "$(DEV_ENV_URL)"
# pac solution export --name "$(SOLUTION_NAME)" --path ./solutions --async
# displayName: 'Export solution from Dev'
# - task: PublishPipelineArtifact@1
# inputs:
# targetPath: 'solutions'
# artifact: 'solution'
#
# - stage: ImportSolution
# dependsOn: ExportSolution
# jobs:
# - job: ImportSolution
# steps:
# - task: DownloadPipelineArtifact@2
# inputs:
# artifact: 'solution'
# path: 'solutions'
# - script: |
# pac auth create --applicationId "$(APP_ID)" --clientSecret "$(CLIENT_SECRET)" --tenant "$(TENANT_ID)" --environment "$(STAGING_ENV_URL)"
# pac solution import --path ./solutions/$(SOLUTION_NAME).zip --async
# displayName: 'Import solution to Staging'Set these as secret pipeline variables in ADO (Pipelines → Library → Variable Groups, or per-pipeline Variables):
| Variable | Description | Secret? |
|---|---|---|
APP_ID |
Service principal Application (client) ID | Yes |
CLIENT_SECRET |
Service principal client secret | Yes |
TENANT_ID |
Azure AD tenant ID | No |
DEV_ENV_URL |
Dev environment URL (e.g., https://contoso-dev.crm.dynamics.com) |
No |
STAGING_ENV_URL |
Staging environment URL | No |
PROD_ENV_URL |
Production environment URL | No |
IMPORTANT: These steps cannot be automated from Claude and must be done manually in the ADO portal:
- Create service connection (optional but recommended): ADO Project → Project Settings → Service Connections → New service connection → Azure Resource Manager
- Add approval gates: ADO → Pipelines → Environments →
staging→ Approvals and checks → Add approval → specify approvers - Add approval gates for production: same for
productionenvironment - Grant pipeline permission to agent pool: ADO → Project Settings → Agent Pools → select pool → Security → grant pipeline access
- Grant pipeline permission to environments: ADO → Pipelines → Environments → select environment → Security → grant pipeline access
⚠️ Coming Soon — Thesetup-pipelineGitHub/ADO path is not yet implemented.
# .github/workflows/deploy.yml
# Power Pages CI/CD Workflow
# Requires repository secrets: APP_ID, CLIENT_SECRET, TENANT_ID
# Requires environment secrets: DEV_ENV_URL, STAGING_ENV_URL, PROD_ENV_URL
name: Deploy Power Pages Site
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
workflow_dispatch:
env:
NODE_VERSION: '20.x'
# SOLUTION_NAME: 'ContosoSite' # Uncomment if using solution-based deployment
jobs:
# ─── Build ────────────────────────────────────────────────────────────────
build:
name: Build Site
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: site-build
path: dist/
retention-days: 5
# ─── Deploy to Dev ─────────────────────────────────────────────────────────
deploy-dev:
name: Deploy to Dev
runs-on: ubuntu-latest
needs: build
environment: dev
if: github.event_name != 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: site-build
path: dist/
- name: Install PAC CLI
run: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
- name: Authenticate PAC CLI
run: |
pac auth create \
--applicationId "${{ secrets.APP_ID }}" \
--clientSecret "${{ secrets.CLIENT_SECRET }}" \
--tenant "${{ secrets.TENANT_ID }}" \
--environment "${{ vars.DEV_ENV_URL }}"
- name: Deploy to Dev
run: pac pages upload-code-site --rootPath "."
# ─── Deploy to Staging ─────────────────────────────────────────────────────
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: deploy-dev
environment: staging
# GitHub environment protection rules handle approvals
# Configure at: Settings → Environments → staging → Protection rules
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: site-build
path: dist/
- name: Install PAC CLI
run: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
- name: Authenticate PAC CLI
run: |
pac auth create \
--applicationId "${{ secrets.APP_ID }}" \
--clientSecret "${{ secrets.CLIENT_SECRET }}" \
--tenant "${{ secrets.TENANT_ID }}" \
--environment "${{ vars.STAGING_ENV_URL }}"
- name: Deploy to Staging
run: pac pages upload-code-site --rootPath "."
# ─── Deploy to Production ──────────────────────────────────────────────────
deploy-prod:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
environment: production
# GitHub environment protection rules handle approvals
# Configure at: Settings → Environments → production → Protection rules
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: site-build
path: dist/
- name: Install PAC CLI
run: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
- name: Authenticate PAC CLI
run: |
pac auth create \
--applicationId "${{ secrets.APP_ID }}" \
--clientSecret "${{ secrets.CLIENT_SECRET }}" \
--tenant "${{ secrets.TENANT_ID }}" \
--environment "${{ vars.PROD_ENV_URL }}"
- name: Deploy to Production
run: pac pages upload-code-site --rootPath "."
# ─── Solution Export/Import (uncomment to enable solution-based deployment) ──
# Add these jobs between build and deploy-dev if using Dataverse solutions:
#
# export-solution:
# needs: build
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - run: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
# - run: |
# pac auth create --applicationId "${{ secrets.APP_ID }}" --clientSecret "${{ secrets.CLIENT_SECRET }}" --tenant "${{ secrets.TENANT_ID }}" --environment "${{ vars.DEV_ENV_URL }}"
# mkdir -p solutions
# pac solution export --name "${{ env.SOLUTION_NAME }}" --path ./solutions --async
# - uses: actions/upload-artifact@v4
# with:
# name: solution
# path: solutions/
#
# import-solution:
# needs: export-solution
# runs-on: ubuntu-latest
# environment: staging
# steps:
# - uses: actions/download-artifact@v4
# with: { name: solution, path: solutions/ }
# - run: dotnet tool install --global Microsoft.PowerApps.CLI.Tool
# - run: |
# pac auth create --applicationId "${{ secrets.APP_ID }}" --clientSecret "${{ secrets.CLIENT_SECRET }}" --tenant "${{ secrets.TENANT_ID }}" --environment "${{ vars.STAGING_ENV_URL }}"
# pac solution import --path "./solutions/${{ env.SOLUTION_NAME }}.zip" --asyncRepository Secrets (Settings → Secrets and variables → Actions → Secrets):
| Secret | Description |
|---|---|
APP_ID |
Service principal Application (client) ID |
CLIENT_SECRET |
Service principal client secret |
TENANT_ID |
Azure AD tenant ID |
Environment Variables (Settings → Environments → {env name} → Environment variables):
| Variable | Dev | Staging | Prod |
|---|---|---|---|
DEV_ENV_URL |
https://contoso-dev.crm.dynamics.com |
— | — |
STAGING_ENV_URL |
— | https://contoso-staging.crm.dynamics.com |
— |
PROD_ENV_URL |
— | — | https://contoso.crm.dynamics.com |
IMPORTANT: These steps cannot be automated and must be done in GitHub:
- Create environments: Settings → Environments → New environment (create
dev,staging,production) - Add protection rules for staging: Settings → Environments → staging → Protection rules → Required reviewers → add approvers
- Add protection rules for production: same for
production - Add secrets: Settings → Secrets and variables → Actions → New repository secret (for APP_ID, CLIENT_SECRET, TENANT_ID)
- Add environment variables: Settings → Environments → {env} → Add environment variable (for ENV_URL per environment)
HAR-confirmed patterns for creating and running Power Platform Pipelines via the Dataverse OData API. Used by the setup-pipeline (PP Pipelines path) and deploy-pipeline skills.
All API calls target the host environment URL — never the source or target environment URLs. Auth token is obtained via az account get-access-token --resource {hostEnvOrigin} --query accessToken -o tsv.
| Operation | API Version |
|---|---|
| Create/update records, Action calls | v9.0 |
List queries, RetrieveDeploymentPipelineInfo |
v9.1 |
RetrieveSetting |
v9.2 |
Call RetrieveSetting from the dev environment to find the tenant's configured Pipelines host:
GET {devEnvUrl}/api/data/v9.2/RetrieveSetting(SettingName='DefaultCustomPipelinesHostEnvForTenant')
Authorization: Bearer {devEnvToken}
OData-MaxVersion: 4.0
OData-Version: 4.0
Accept: application/json
Returns { "SettingValue": "{BAP-environment-GUID}" } or empty/null if no default is configured.
Cross-reference the GUID with the environment list to find the host environment URL:
node "${PLUGIN_ROOT}/scripts/lib/list-environments.js"This emits a JSON array of { displayName, environmentId, environmentUrl, uniqueName, active }. (It parses pac env list; the older pac env list --output json is invalid on current PAC CLI, which only accepts --filter on env list.) Match the GUID on the environmentId field. If no match, probe each environment from pac env list with:
GET {envUrl}/api/data/v9.1/deploymentpipelines?$top=0
Environments that return 200 (not 404) have the Pipelines package installed.
The deploymentenvironments entity requires the BAP environment ID (a GUID), not the Dataverse organization ID. Get it from pac env list output field EnvironmentId, or from pac env who output. This is different from the Dataverse organizationid.
Create one record per environment (source dev + each target):
POST {hostUrl}/api/data/v9.0/deploymentenvironments
Content-Type: application/json
Authorization: Bearer {hostEnvToken}
{
"name": "{siteName} Development",
"environmentid": "{BAP-environment-GUID}",
"environmenttype": 200000000
}
environmenttypevalues:200000000= source/development,200000001= target. This field is required — omitting it causes a 400 error.
Response: Most creates (deploymentenvironments, deploymentpipelines, deploymentstages) return 204 — parse the created record ID from the
OData-EntityIdresponse header.deploymentstagerunsPOST returns 201 (newer host package — ID in JSON body) or 204 (older package — ID inOData-EntityIdheader). Always implement both paths: try body first, fall back to header.
After creating each deploymentenvironment, poll until validation completes:
GET {hostUrl}/api/data/v9.1/deploymentenvironments({id})?$select=validationstatus
Poll until validationstatus = 200000001 AND statecode = 0 (Active/succeeded). If statecode = 1 with a non-null errormessage: the environment validation failed — report the error and do not continue.
Poll every 3 seconds, max 20 attempts.
POST {hostUrl}/api/data/v9.0/deploymentpipelines
Content-Type: application/json
{
"name": "{pipeline name}",
"description": "Power Pages deployment pipeline for {siteName}"
}
Extract deploymentpipelineid from OData-EntityId response header.
Link the source deployment environment to the pipeline. Use relative path format — not full URL (HAR-confirmed):
POST {hostUrl}/api/data/v9.0/deploymentpipelines({pipelineId})/deploymentpipeline_deploymentenvironment/$ref
Content-Type: application/json
{
"@odata.context": "{hostUrl}/api/data/v9.0/$metadata#$ref",
"@odata.id": "deploymentenvironments({sourceDeploymentEnvironmentId})"
}
Note: Response is 204 (not 200 with entity body as the HAR initially suggested). Treat any 2xx as success.
Note:
@odata.iduses a relative path (no leading/). Do NOT use the fullhttps://...URL — the portal sends the relative form and the API accepts it.
Create one stage per target environment, in deployment order:
POST {hostUrl}/api/data/v9.0/deploymentstages
Content-Type: application/json
{
"name": "Deploy to {targetName}",
"deploymentpipelineid@odata.bind": "/deploymentpipelines({pipelineId})",
"targetdeploymentenvironmentid@odata.bind": "/deploymentenvironments({targetDeploymentEnvironmentId})"
}
Note: The
rankfield does not exist ondeploymentstage. For multi-stage ordering, use"previousdeploymentstageid@odata.bind": "/deploymentstages({previousStageId})"to link stages in a chain (similar to a linked list).
Extract deploymentstagesid from OData-EntityId response header.
Before creating a stage run, call RetrieveDeploymentPipelineInfo to get the source environment ID and available artifacts:
GET {hostUrl}/api/data/v9.1/RetrieveDeploymentPipelineInfo(DeploymentPipelineId={pipelineId},SourceEnvironmentId='{BAP_SOURCE_ENV_ID}',ArtifactName='{solutionName}')
Authorization: Bearer {hostEnvToken}
Where BAP_SOURCE_ENV_ID is the BAP GUID of the dev environment (from pac env list EnvironmentId field, or pac env who).
Returns: SourceDeploymentEnvironmentId, StageRunsDetails[], EnableAIDeploymentNotes, EnableRedeployment, DeploymentType.
Use SourceDeploymentEnvironmentId as the devdeploymentenvironment binding in the stage run. Use solutionId from .solution-manifest.json as the artifact solution ID.
Version note: This function may not exist in older Pipelines package versions (returns 404). Fallback: query the
deploymentpipeline_deploymentenvironmentnavigation property to get the source environment ID:GET {hostUrl}/api/data/v9.1/deploymentpipelines({pipelineId})/deploymentpipeline_deploymentenvironment?$select=deploymentenvironmentid,name,environmenttypeFilter for
environmenttype = 200000000to get the source deployment environment record. Usedeploymentenvironmentidas thesourceDeploymentEnvironmentId.
Create the stage run (note the $select on the URL — required to get the ID back):
POST {hostUrl}/api/data/v9.0/deploymentstageruns?$select=deploymentstagerunid
Content-Type: application/json
{
"deploymentstageid@odata.bind": "/deploymentstages({stageId})",
"devdeploymentenvironment@odata.bind": "/deploymentenvironments({sourceDeploymentEnvironmentId})",
"artifactname": "{solutionUniqueName}",
"solutionid": "{solutionId}",
"makerainoteslanguagecode": "en-US"
}
Note:
deploymentstageidis the correct lookup binding name (notstageid).devdeploymentenvironmentis the correct navigation property for the source deployment environment (notartifactid).artifactnameis required — provide the solution unique name string.solutionidis required (not optional). Use the GUID fromRetrieveDeploymentPipelineInfo.
Then trigger validation — ValidatePackageAsync is a top-level action (not bound to the entity):
POST {hostUrl}/api/data/v9.0/ValidatePackageAsync
Content-Type: application/json
Authorization: Bearer {HOST_TOKEN}
{"StageRunId": "{STAGE_RUN_ID}"}
Returns 204 when available. Returns 404 on older Pipelines package versions → fall back to pac pipeline deploy (see pac pipeline deploy CLI (Fallback / Alternative) below).
Version note:
ValidatePackageAsyncandDeployPackageAsynccustom actions may not exist in older Pipelines package versions. If these return 404, use thepac pipeline deployCLI as the deployment mechanism instead.
Poll until validation completes — use single-entity GET, check stagerunstatus:
GET {hostUrl}/api/data/v9.0/deploymentstageruns({stageRunId})?$select=deploymentstagerunid,stagerunstatus,errormessage,operation,operationdetails,operationstatus,scheduledtime,targetenvironmentid,validationresults,artifactname,deploymentsettingsjson
stagerunstatus values during validation:
200000006= Validating — in-progress, keep polling200000007= Validation Succeeded — terminal success, proceed to next step200000003= Failed — terminal failure, stop and display error200000004= Canceled — terminal, stop200000005= Pending Approval — pause and inform user to approve in Power Platform make.powerapps.com portal, then re-poll after user confirms
operation field reference values:
| Value | Label |
|---|---|
| 200000200 | None (not started) |
| 200000201 | Validate |
| 200000202 | Deploy |
Important:
validationresultsis a double-encoded JSON string — callJSON.parse()on it twice (or once afterJSON.parse()of the OData response body) to get the object. The object has shape:{ ValidationStatus, SolutionValidationResults: [{ SolutionValidationResultType, Message, ErrorCode }], SolutionDetails, MissingDependencies }.
Surface any SolutionValidationResults entries to the user as warnings. Known error codes:
ErrorCode: -2147188672— managed/unmanaged conflict: "The solution is already installed as unmanaged but this package is managed." The user must uninstall the existing solution from the target environment before retrying.
Fetch AI-generated deployment notes (if EnableAIDeploymentNotes = true from RetrieveDeploymentPipelineInfo):
GET {hostUrl}/api/data/v9.0/deploymentstageruns({stageRunId})?$select=aigenerateddeploymentnotes,deploymentstagerunid
Store the value as AI_DEPLOY_NOTES.
If the solution contains environment variables or connection references that need target-environment values, PATCH the stage run between Validate and Deploy:
PATCH {hostUrl}/api/data/v9.0/deploymentstageruns({stageRunId})
Content-Type: application/json
{
"deploymentsettingsjson": "{...JSON string with env var overrides and connection ref mappings...}"
}
The deploymentsettingsjson value is a JSON-serialized string (not a nested object). Structure:
{
"EnvironmentVariables": [
{ "SchemaName": "prefix_VarName", "Value": "target-value" }
],
"ConnectionReferences": [
{ "LogicalName": "prefix_ConnRefName", "ConnectionId": "target-connection-id" }
]
}Before calling DeployPackageAsync, PATCH the stage run with version info and deployment notes:
PATCH {hostUrl}/api/data/v9.0/deploymentstageruns({stageRunId})
Content-Type: application/json
{
"artifactdevcurrentversion": "{current version in source env — query GET solutions?$filter=uniquename eq '...'&$select=version}",
"artifactversion": "{new version — must be strictly > version already deployed in target stage}",
"deploymentnotes": "{AI_DEPLOY_NOTES if available, otherwise a brief description of what is being deployed}"
}
Returns HTTP 204.
Version accuracy is critical:
artifactdevcurrentversionmust match the liveversionfield of the solution in the source environment (query it — do not use stale values from.solution-manifest.json).artifactversionmust be strictly greater than the version already in the target stage — checkdocs/alm/last-deploy.jsonfor the last deployed version and increment from there.
Trigger deployment — DeployPackageAsync is a top-level action (not bound to the entity):
POST {hostUrl}/api/data/v9.0/DeployPackageAsync
Content-Type: application/json
{"StageRunId": "{STAGE_RUN_ID}"}
Returns HTTP 204.
Poll stagerunstatus until terminal — use filter GET pattern during deployment:
GET {hostUrl}/api/data/v9.0/deploymentstageruns?$filter=(deploymentstagerunid eq {stageRunId})&$select=_deploymentstageid_value,deploymentstagerunid,stagerunstatus,operation,operationstatus,suboperation,artifactname
stagerunstatus values during deployment:
200000010= Deploying — in-progress, keep polling (every 10 seconds, max 120 attempts)200000002= Succeeded — terminal success200000003= Failed — terminal failure200000004= Canceled — terminal200000005= Pending Approval — pause and inform user to approve in Power Platform make.powerapps.com portal, then re-poll after user confirms
suboperation field values during deploy:
| Value | Label |
|---|---|
| 200000100 | None (starting/finishing) |
| 200000105 | Deploying Artifact (actively installing solution) |
If stagerunstatus = 200000005 (Pending Approval): pause and inform user to approve in Power Platform make.powerapps.com portal, then re-poll after user confirms.
POST {hostEnvUrl}/api/data/v9.1/RetryFailedDeploymentAsync
Content-Type: application/json
Authorization: Bearer {HOST_TOKEN}
{"StageRunId": "{STAGE_RUN_ID}"}
→ HTTP 204
Call this instead of creating a new stage run when retrying a failed deployment. Then resume polling stagerunstatus as in the deploy phase.
PATCH {hostEnvUrl}/api/data/v9.0/deploymentstageruns({STAGE_RUN_ID})
Content-Type: application/json
Authorization: Bearer {HOST_TOKEN}
{"iscanceled": true}
→ HTTP 204
Poll every 2 seconds, max 30 attempts (~1 minute):
validationstatus = 200000000→ Pending, keep pollingvalidationstatus = 200000001→ Succeeded ✓statecode = 1with non-nullerrormessage→ Failed ✗
Add scheduledtime to the stage run POST body for a scheduled future deployment:
{
"deploymentstageid@odata.bind": "...",
"devdeploymentenvironment@odata.bind": "...",
"artifactname": "...",
"solutionid": "...",
"scheduledtime": "2026-04-01T10:00:00Z"
}{
"deploymentstageid@odata.bind": "...",
"devdeploymentenvironment@odata.bind": "...",
"artifactname": "...",
"artifactid@odata.bind": "/deploymentartifacts({artifactId})",
"isredeployment": true
}Fetch prior successful deployments to show as redeployment options:
GET {hostEnvUrl}/api/data/v9.1/deploymentstageruns
?$filter=((stagerunstatus eq 200000002))
&$orderby=starttime desc
&$select=artifactname,artifactversion,deploymentstagerunid,_artifactid_value,...
GET {hostEnvUrl}/api/data/v9.0/deploymentartifacts({artifactId})/artifactfile/$value → managed zip
GET {hostEnvUrl}/api/data/v9.0/deploymentartifacts({artifactId})/artifactfileunmanaged/$value → unmanaged zip
The platform host is auto-provisioned on demand via the BAP RP API (not the Dataverse OData API):
POST {BapRpEndpoint}/environments/getOrCreate?api-version=2021-04-01
Content-Type: application/json
{
"properties": {
"environmentSku": "Platform",
"linkedEnvironmentMetadata": {
"templates": ["D365_1stPartyAdminApps"]
}
}
}
Returns 202 with location and retry-after headers. Poll location until provisioningState = "Succeeded".
DefaultCustomPipelinesHostEnvForTenant defaults to '' (empty string) when using platform host — treat any falsy/empty value as "platform host in use."
Used to take over an environment's pipelines-host association when creating a deploymentenvironments record fails with "this environment is already associated with another pipelines host". HAR-verified against the AppDeploymentConfiguration UI on supplierportalpipelineshostch.crm17 (2026-05-11). Documented at alm/custom-host-pipelines#using-force-link….
Endpoint:
POST {hostEnvUrl}/api/data/v9.0/ManageEnvironmentStamp
Authorization: Bearer <hostToken>
Content-Type: application/json
Accept: application/json
clienthost: Browser
prefer: odata.include-annotations="*"
x-ms-app-name: AppDeploymentConfiguration
{ "DeploymentEnvironmentId": "{C44399FE-BF4A-F111-BEC6-7CED8D42BEFA}" }
Note the GUID is wrapped in {UPPER-CASE-BRACES} — the only shape observed in production.
Success: 204 No Content. The action is synchronous, but the record's validationstatus re-evaluates asynchronously. Poll afterward:
GET {hostEnvUrl}/api/data/v9.0/deploymentenvironments({deploymentEnvironmentId})?$select=validationstatus,errormessage,name
until validationstatus = 200000001 (Succeeded). 200000002 (Failed) means the stamp move was rejected (e.g., previous host has reapply policy); surface errormessage verbatim.
Failure modes:
| Status | Cause | Remediation |
|---|---|---|
| 403 | Caller lacks Deployment Pipeline Administrator on the target host | Host admin must grant the role |
| 404 | No deploymentenvironments record exists on the target host for this BAP env |
Create it first (which will create it in a Failed state with the "already associated" errormessage — that's the trigger for Force Link) |
Post-poll validationstatus = Failed |
Previous host still claims the env | Show errormessage to the user; may require admin intervention on the previous host |
Side effects (documented on Microsoft Learn):
- Previous host's
deploymentenvironmentsrow for this BAP env is delinked; itsvalidationstatusis left stale until refreshed in the previous host's UI. - Makers in the previous host lose access to pipelines they ran through this environment.
- Reversible by performing Force Link again from the previous host.
Plugin implementation: scripts/lib/force-link-environment.js wraps the POST + post-validation polling. Surface as auto-fix for Pattern 15 in deployment-error-catalog.md via the /power-pages:force-link-environment skill.
When ValidatePackageAsync / DeployPackageAsync are unavailable (older Pipelines package), or when the deployment environment is configured via Power Platform Admin Center, use the PAC CLI:
pac pipeline deploy \
--environment "{devEnvUrl}" \
--solutionName "{solutionUniqueName}" \
--stageId "{deploymentstagesid}" \
--currentVersion "{currentVersion}" \
--newVersion "{newVersion}" \
--waitPrerequisites for CLI deployment:
- The dev environment must have a PP Pipelines host configured (via Power Platform Admin Center or
DefaultCustomPipelinesHostEnvForTenanttenant setting). Without this, the CLI returns "Resource not found for the segment 'deploymentenvironments'". --currentVersionand--newVersionmust be valid semver strings (e.g.,1.0.0.0,1.0.0.1).
Written by setup-pipeline (PP Pipelines path) after successful pipeline creation:
{
"pipelineId": "{deploymentpipelineid}",
"pipelineName": "{pipeline name}",
"hostEnvUrl": "{hostEnvUrl}",
"sourceDeploymentEnvironmentId": "{sourceDeploymentEnvironmentId}",
"sourceEnvironmentUrl": "{devEnvUrl}",
"solutionName": "{solutionUniqueName}",
"createdAt": "{ISO timestamp}",
"stages": [
{
"stageId": "{deploymentstagesid}",
"name": "Deploy to Staging",
"rank": 1,
"targetDeploymentEnvironmentId": "{targetDeploymentEnvironmentId}",
"targetEnvironmentUrl": "{stagingEnvUrl}"
}
]
}Written by deploy-pipeline after each deployment run:
{
"pipelineId": "{deploymentpipelineid}",
"stageId": "{deploymentstagesid}",
"stageRunId": "{deploymentstagerunid}",
"stageName": "Deploy to Staging",
"solutionName": "{solutionUniqueName}",
"solutionId": "{solutionId}",
"status": "Succeeded",
"deployedAt": "{ISO timestamp}",
"hostEnvUrl": "{hostEnvUrl}"
}