feat: add CI/CD pipeline for CodeApp deployment and management

This commit is contained in:
2026-07-12 15:10:29 +02:00
parent 7c298a26b8
commit 172e72dbcd
2 changed files with 363 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
trigger:
branches:
include:
- master
paths:
include:
- CodeApp_NeonReactor/src/**
- CodeApp_NeonReactor/public/**
- CodeApp_NeonReactor/index.html
- CodeApp_NeonReactor/power.config.json
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build App
jobs:
- job: Build
displayName: Build React App
steps:
- task: UseNode@1
inputs:
version: '$(NODE_VERSION)'
displayName: 'Install Node.js'
- script: |
npm ci
displayName: 'Install dependencies'
workingDirectory: '$(CODEAPP_DIR)'
- script: |
npm run build
displayName: 'Build application'
workingDirectory: '$(CODEAPP_DIR)'
- stage: Deploy
displayName: Deploy to Power Platform
dependsOn: Build
condition: succeeded()
jobs:
- job: Deploy
displayName: Deploy Code App
steps:
- checkout: self
persistCredentials: true
- task: UseNode@1
inputs:
version: '$(NODE_VERSION)'
displayName: 'Install Node.js'
- script: |
npm ci
displayName: 'Install dependencies'
workingDirectory: '$(CODEAPP_DIR)'
- script: |
npm run build
displayName: 'Build application'
workingDirectory: '$(CODEAPP_DIR)'
- script: |
set -euo pipefail
dotnet tool install --global Microsoft.PowerApps.CLI.Tool --version "$(PAC_CLI_VERSION)"
export PATH="$PATH:$HOME/.dotnet/tools"
if [ -z "$(ENVIRONMENT_URL)" ]; then
echo "ENVIRONMENT_URL is empty. Set it to your Dataverse org URL (example: https://orgname.crm.dynamics.com)."
exit 1
fi
if [ -z "$(SERVICE_USERNAME)" ]; then
echo "SERVICE_USERNAME is empty. Set it in Azure DevOps Pipeline settings."
exit 1
fi
if [ -z "$(SERVICE_PASSWORD)" ]; then
echo "SERVICE_PASSWORD is empty. Set it as a secret variable in Azure DevOps Pipeline settings."
exit 1
fi
if [ -z "$(PAC_CLI_VERSION)" ]; then
echo "PAC_CLI_VERSION is empty. Set it as a pipeline variable in Azure DevOps."
exit 1
fi
if [ ! -d "./dist" ]; then
echo "Build output ./dist not found. Ensure the deploy job runs npm run build or downloads build artifacts."
exit 1
fi
pac auth clear
pac auth create --name "CI" --username "$(SERVICE_USERNAME)" --password "$(SERVICE_PASSWORD)" --tenant "$(TENANT_ID)"
pac org select --environment "$(ENVIRONMENT_URL)"
PAC_ACCESS_LOG="$(mktemp)"
pac code list 2>&1 | tee "$PAC_ACCESS_LOG"
if grep -Eiq "HTTP error status:\s*403|does not have permission to access the path|Forbidden" "$PAC_ACCESS_LOG"; then
echo "PAC access check failed for this service user in target environment."
exit 1
fi
CURRENT_APP_ID="$(node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('power.config.json','utf8'));console.log(c.appId ?? '')")"
CURRENT_APP_NAME="$(node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('power.config.json','utf8'));console.log(c.appDisplayName)")"
if [ -z "$CURRENT_APP_ID" ] || [ "$CURRENT_APP_ID" = "null" ]; then
EXISTING_APP_ID="$(awk -v app="$CURRENT_APP_NAME" 'index($0, app)>0 && $1 ~ /^[0-9a-fA-F-]{36}$/ {print $1; exit}' "$PAC_ACCESS_LOG")"
if [ -n "$EXISTING_APP_ID" ]; then
echo "Resolved existing appId '$EXISTING_APP_ID' from app display name '$CURRENT_APP_NAME'."
node -e "const fs=require('fs');const p='power.config.json';const c=JSON.parse(fs.readFileSync(p,'utf8'));c.appId='$EXISTING_APP_ID';fs.writeFileSync(p, JSON.stringify(c,null,2)+'\\n');"
CURRENT_APP_ID="$EXISTING_APP_ID"
else
echo "No existing app found for '$CURRENT_APP_NAME'. pac code push will create one."
fi
fi
max_attempts=3
attempt=1
push_succeeded=0
app_not_found_recovery_done=0
while [ "$attempt" -le "$max_attempts" ]; do
echo "PAC push attempt $attempt/$max_attempts"
PAC_PUSH_LOG="$(mktemp)"
pac code push 2>&1 | tee "$PAC_PUSH_LOG"
if grep -Eiq "HTTP error status:\s*403|does not have permission to access the path|Forbidden" "$PAC_PUSH_LOG"; then
echo "PAC deployment failed due to insufficient service user permissions in the target environment."
exit 1
fi
if grep -Eiq "ApplicationNotFound" "$PAC_PUSH_LOG"; then
if [ "$app_not_found_recovery_done" -eq 0 ] && [ -n "$CURRENT_APP_ID" ] && [ "$CURRENT_APP_ID" != "null" ]; then
echo "Configured appId '$CURRENT_APP_ID' was not found. Clearing appId and retrying once as app creation flow."
node -e "const fs=require('fs');const p='power.config.json';const c=JSON.parse(fs.readFileSync(p,'utf8'));c.appId=null;fs.writeFileSync(p, JSON.stringify(c,null,2)+'\\n');"
CURRENT_APP_ID=""
app_not_found_recovery_done=1
attempt=$((attempt + 1))
continue
fi
fi
if grep -Eiq "HTTP error status:\s*5[0-9]{2}|InternalServerError" "$PAC_PUSH_LOG"; then
if [ "$attempt" -lt "$max_attempts" ]; then
echo "PAC deployment hit a transient server error (5xx). Retrying in 15 seconds..."
attempt=$((attempt + 1))
sleep 15
continue
fi
fi
if grep -Eiq "HTTP error status:" "$PAC_PUSH_LOG"; then
echo "PAC deployment failed. See HTTP error details in the logs above."
exit 1
fi
push_succeeded=1
break
done
if [ "$push_succeeded" -ne 1 ]; then
echo "PAC deployment did not succeed after $max_attempts attempts."
exit 1
fi
PUSH_APP_ID="$(sed -nE 's#.*app/([0-9a-fA-F-]{36}).*#\1#p' "$PAC_PUSH_LOG" | head -n1)"
if [ -n "$PUSH_APP_ID" ] && ([ -z "$CURRENT_APP_ID" ] || [ "$CURRENT_APP_ID" = "null" ]); then
echo "Persisting newly created appId '$PUSH_APP_ID' into power.config.json."
node -e "const fs=require('fs');const p='power.config.json';const c=JSON.parse(fs.readFileSync(p,'utf8'));c.appId='$PUSH_APP_ID';fs.writeFileSync(p, JSON.stringify(c,null,2)+'\\n');"
fi
if ! git diff --quiet -- power.config.json; then
echo "Committing updated power.config.json back to repository."
git config user.email "azure-pipelines-bot@local"
git config user.name "azure-pipelines-bot"
git add power.config.json
git commit -m "ci: persist CodeApp appId [skip ci]"
git push origin HEAD:master
else
echo "No appId changes to persist."
fi
displayName: 'Authenticate and Deploy to Power Platform'
workingDirectory: '$(CODEAPP_DIR)'