Power Platform solutions make it easier to package and move apps, flows, tables, and other components between environments. A reliable Application Lifecycle Management (ALM) process adds source control, validation, repeatable builds, environment-specific configuration, testing, and a controlled Production release.
This guide walks through an end-to-end implementation using three Power Platform environments—Dev, QA, and Production—and GitHub Actions. It is designed so you can adapt the setup to your own Microsoft tenant and repository.
The example solution is called EmployeeALMDemo. It is a sample solution for a proof of concept (POC); you can use your own solution by replacing the example name and configuring its dependencies. The guide focuses on the ALM setup and release process rather than explaining how to create each app, flow, or table inside the sample solution.
1.What we are building
The goal is to move a solution through a consistent lifecycle:
- Develop and validate changes in Dev.
- Export the solution from Dev and unpack it into source files.
- Review the source changes in a pull request (PR).
- Run automated checks before merging.
- Build a managed solution from the reviewed GitHub source.
- Deploy that build to QA and test it.
- Promote the same managed artifact to Production after the required approval.
Scroll sideways to see the whole diagram.
High-level lifecycle
- Power Platform Dev environmentWhere changes are madeExport both ZIPs and unpack (Both)
- GitHub export branch + PRUnpacked solution sourcePR validation
- Build and CheckSource + settings + CheckerReview and merge
- main branchReviewed source of truthBuild from source
- Managed solution artifactmanaged-solution, kept for 30 daysManual QA deployment
- QA environmentImport + functional testingSame build ID + approval
- Production environmentImport + smoke testing
Why use separate environments?
| Environment | Main purpose |
|---|---|
| Dev | Build and modify the unmanaged solution |
| QA | Validate the managed solution in a test environment |
| Production | Run the approved release for business users |
An unmanaged solution is normally used during development. Managed solutions are used for downstream deployment. The Git repository stores the unpacked source, and a release artifact should be traceable to the reviewed source revision.
2.Why this guide stores unpacked source in Git
There are two broad strategies for keeping a Power Platform solution in GitHub.
| Area | Approach 1: Export and store a solution ZIP | Approach 2: Store unpacked source in Git |
|---|---|---|
| Git review | ZIP-level changes are difficult to review | Individual files can be reviewed |
| Pull requests | Limited visibility into component changes | Component-level source diffs |
| Merge conflicts | Difficult to resolve inside ZIPs | Git can merge many text-based files |
| Build process | Often relies on exported package files | Rebuild the package from reviewed source |
| Traceability | Package is the primary tracked item | Source changes and commits are traceable |
| Best fit | Simple package backup or transfer | Source-controlled ALM and team development |
This guide uses Approach 2 because the goal is a proper GitHub-based ALM pipeline with code review, validation, repeatable builds, and controlled deployments.
One important detail: unpacked solution files are not all plain text. Some components may contain binary or generated files, so Git diffs and merge support depend on the component type.
One source folder for both package types
The export workflow exports two ZIP files from Dev, EmployeeALMDemo.zip (unmanaged) and EmployeeALMDemo_managed.zip (managed), and keeps them side by side. It then unpacks them into a single source folder:
pac solution unpack \
--zipfile "$UNMANAGED_ZIP" \
--folder "$SOURCE_FOLDER" \
--packagetype Both \
--clobberEmployeeALMDemo.zipUnmanaged export from DevEmployeeALMDemo_managed.zip Managed export from Dev--packagetype Both
solutions/EmployeeALMDemo/ One source folder in Git, reviewed in pull requestsas Managed
managed-solutionArtifact with EmployeeALMDemo_managed.zip for QA and ProductionWhat does Both mean? The --packagetype Both option requests PAC CLI to unpack the solution in a format intended to support both managed and unmanaged package types. The matching *_managed.zip must sit beside the unmanaged ZIP when the command runs. The pipeline should verify that the combined source can successfully be packed into both types; exporting both ZIPs alone does not prove that every managed-specific difference has been incorporated.
The expected source structure is approximately:
powerplatform-alm-demo/
├── .github/
│ └── workflows/
├── deployment-settings/
│ ├── qa.json
│ └── prod.json
├── solutions/
│ └── EmployeeALMDemo/
│ ├── Other/
│ │ └── Solution.xml
│ ├── Entities/
│ ├── Workflows/
│ └── ...
└── README.mdThe exact component directories depend on your solution. Files that differ between the package types are stored next to each other, for example AppModule.xml and AppModule_managed.xml. You do not maintain separate Git source folders for managed and unmanaged solutions under this approach.
3.Workflow overview
This implementation uses separate workflows so each stage has a clear responsibility.
| Workflow file | Trigger / use | Purpose |
|---|---|---|
.github/ | Manual | Export Dev solution, unpack it, and open a PR |
.github/ | Pull request targeting main | Validate source and deployment-settings files; pack and run Solution Checker |
.github/ | Merge/push to main or manual run, depending on configured triggers | Build a managed solution from the version-controlled source and publish an artifact |
.github/ | Manual | Deploy a selected managed build to QA |
.github/ | Manual, with Production environment protection | Deploy the exact build tested in QA after approval |
.github/ | Scheduled and/or push/PR, depending on configuration | Analyze supported repository code, including GitHub Actions workflows |
The QA and Production deployments are manual in this implementation. This makes the release decision explicit while the pipeline is being introduced.
Responsibilities at a glance
export-dev-solution.yml- Dev
- unmanaged + managed ZIPs
- unpacked source (Both)
- export branch
- PR
build-and-check.yml- PR
- source validation
- settings JSON validation
- package validation
- Solution Checker
- PR status
build-managed-solution.yml- main source
- pack as Managed
- managed-solution artifact
deploy-qa.yml- selected build artifact
- QA import
- QA evidence
deploy-prod.yml- exact QA-tested build
- Production approval
- Production import
4.Prerequisites
Prepare these items before configuring the workflows.
Microsoft Power Platform
- A Microsoft Entra tenant.
- Access to Power Platform admin center.
- Three Dataverse environments: Dev, QA, and Production.
- A Dataverse database in each environment.
- The solution and its required dependencies available in Dev.
- Permission to register an application in Microsoft Entra ID.
- Permission to add an application user and assign an appropriate security role in each environment.
- Appropriate Power Platform licensing and environment capacity.
GitHub
- A GitHub account and repository.
- GitHub Actions enabled for the repository.
- Permission to configure repository secrets, variables, environments, and branch protection.
- A fine-grained personal access token (PAT) for the export workflow to push a branch and create a PR, if using the PAT approach described in this guide.
Local tools for preparing the solution
- Windows PowerShell.
- Power Platform CLI (
pac). - Access to the Dev environment and permission to export the sample solution.
Example names used in this guide
Replace these example values with the values for your own setup.
| Item | Example |
|---|---|
| Solution unique name | EmployeeALMDemo |
| Publisher display name | ALM Demo Publisher |
| Publisher prefix | ald |
| Repository | powerplatform-alm-demo |
| Main branch | main |
| GitHub QA environment | QA |
| GitHub Production environment | Production |
Use your own environment URLs. For illustration, this guide uses dummy values such as https:/; these are examples, not working tenant URLs.
5.Create or identify the Power Platform environments
Open the Power Platform admin center and create or identify the three environments.
Record the Dataverse URL for each environment:
Dev: https://your-dev.crm.dynamics.com
QA: https://your-qa.crm.dynamics.com
Production: https://your-prod.crm.dynamics.comUse the exact URLs shown for your environments. These URLs will be stored in GitHub Actions secrets.
Confirm that the sample solution can be exported from Dev and that the dependencies required by the solution are available in the target environments.
6.Register an application in Microsoft Entra ID
GitHub Actions needs a non-interactive identity to authenticate to Dataverse. This guide uses an application registration and client secret.
- Open the Microsoft Entra admin center.
- Go to Identity → Applications → App registrations.
- Select New registration.
- Give the application a name, for example
PowerPlatform-ALM-GitHub. - Select the single-tenant account option appropriate for your organization and register the application.
- From the Overview page, record the Application (client) ID and Directory (tenant) ID.
- Under Certificates & secrets, create a client secret and securely copy its Value.
The client secret value is what the workflow needs; it is not the secret’s ID. Store it securely and plan for rotation before expiry. Do not commit credentials to GitHub or paste them into workflow YAML.
For production-grade deployments, assess whether certificate-based or federated authentication is suitable for your organization.
Reference: Register an application with Microsoft Entra ID for Dataverse.
7.Add the application user to Dev, QA, and Production
An Entra application registration alone does not grant Dataverse access. Add the application as an application user separately in each environment.
For Dev, QA, and Production:
- Open the environment in Power Platform admin center.
- Open the environment’s application-user management area.
- Add an application user and select the app registration created in the previous section.
- Select the appropriate business unit.
- Assign a security role with the permissions required for the operations that the workflows perform.
- Save the application user.
Use least privilege and verify that the role supports the solution export/import and validation operations used by your pipeline. Access in Dev does not automatically grant access to QA or Production.
The workflows use Power Platform authentication and a who-am-i check to confirm that the service principal can connect to the intended environment.
8.Create the GitHub repository and source structure
Create a repository for the ALM project. For example, name it powerplatform-alm-demo.
A recommended structure is:
powerplatform-alm-demo/
├── .github/
│ └── workflows/
│ ├── export-dev-solution.yml
│ ├── build-and-check.yml
│ ├── build-managed-solution.yml
│ ├── deploy-qa.yml
│ ├── deploy-prod.yml
│ └── codeql.yml
├── solutions/
│ └── EmployeeALMDemo/
├── deployment-settings/
│ ├── qa.json
│ └── prod.json
└── README.mdThe solutions/ directory contains the unpacked solution source, not just an exported ZIP. The workflow files live under .github/workflows/, where GitHub Actions can discover them.
You can use the sample EmployeeALMDemo solution for a POC or substitute your own solution. The ALM process does not require you to reproduce the sample’s individual apps, flows, or tables.
9.Configure GitHub repository variables and secrets
Open:
Repository → Settings → Secrets and variables → Actions
9.1 Repository variable
Create this repository variable:
| Variable | Example value |
|---|---|
PP_SOLUTION_NAME | EmployeeALMDemo |
Use the solution’s unique name, not necessarily the display name shown to users.
9.2 Repository secrets for Power Platform
Create the following repository secrets:
| Secret | Value |
|---|---|
PP_APP_ID | Microsoft Entra application/client ID |
PP_TENANT_ID | Microsoft Entra tenant ID |
PP_CLIENT_SECRET | Client secret value |
DEV_ENVIRONMENT_URL | Dev Dataverse URL |
QA_ENVIRONMENT_URL | QA Dataverse URL |
PROD_ENVIRONMENT_URL | Production Dataverse URL |
The workflows reference these values with expressions such as:
${{ secrets.PP_CLIENT_SECRET }}
${{ secrets.QA_ENVIRONMENT_URL }}
${{ vars.PP_SOLUTION_NAME }}Do not hard-code environment URLs or credentials into workflow code if they should be configurable per repository or tenant.
9.3 Configure the GitHub token for export branch and PR creation
The export workflow needs GitHub permissions to push its export branch and open a pull request. If you use a fine-grained PAT:
- Open GitHub fine-grained personal access tokens.
- Give the token a recognizable name, such as
PowerPlatform-ALM-Automation. - Select the repository owner that owns your ALM repository.
- Restrict the token to the specific repository.
- Grant Contents: Read and write and Pull requests: Read and write. Metadata access is normally read-only.
- Set an expiry consistent with your organization’s policy and record a rotation reminder.
- In the repository, open Settings → Secrets and variables → Actions → New repository secret.
- Name the secret
ALM_GITHUB_TOKENand paste the token value.
The token should be limited to the repository and permissions required by the workflow. Do not paste the PAT into YAML or commit it to source control. The PAT is used for GitHub operations; Power Platform authentication continues to use the Entra application credentials.
10.Protect the main branch
The main branch represents reviewed source intended for release. Changes should arrive through pull requests.
Open Repository → Settings → Rules → Rulesets (or the equivalent classic branch protection settings) and configure a rule targeting main.
Recommended configuration:
- Require a pull request before merging.
- Require the relevant validation status check to pass.
- Use
Build and Check /as the required check if that is the check name produced by your workflow.Validate Power Platform Solution - Require branches to be up to date before merging if it fits your team’s workflow.
- Restrict bypasses so the intended review process is enforced.
- Configure reviewer approval according to your team’s policy.
The status check may not appear in the branch protection picker until the workflow has run successfully at least once. For a personal POC, you can choose an approval policy suitable for a solo repository while still requiring automated validation.
Reference: GitHub protected branches.
11.Configure GitHub deployment environments
Open Repository → Settings → Environments and create these GitHub environments:
QAProduction
Use the same names in the workflow YAML.
QA environment
Create the QA environment. If desired and supported by your repository configuration, restrict deployments to main.
Production environment
Create the Production environment and configure required reviewers. Enable prevention of self-review and restrict deployment branches to main where those options are available.
The Production workflow must reference the Production environment, and the protection rule must be configured in GitHub settings. Adding environment: Production to YAML alone does not configure a required approval.
Reference: Control deployments with GitHub Actions environments.
12.Prepare deployment settings for QA and Production
Power Platform deployment settings supply environment-specific values for environment variables and connection references when importing a solution. Keep separate settings files for each target environment.
Expected files:
deployment-settings/
├── qa.json
└── prod.json12.1 Authenticate the Power Platform CLI to Dev
Open PowerShell and run the following commands. Replace the sample URL with your own Dev environment URL.
pac auth create --url "https://your-dev.crm.dynamics.com"
pac auth list
pac auth whopac auth createcreates an authentication profile.pac auth listlists available profiles.pac auth whodisplays the identity associated with the selected profile.
Use the profile associated with the correct environment before exporting the solution.
12.2 Export the unmanaged solution
Create a working folder and move into it:
New-Item -ItemType Directory -Path "C:\PowerPlatformALM" -Force
Set-Location "C:\PowerPlatformALM"Export the sample solution as an unmanaged ZIP:
pac solution export --name EmployeeALMDemo --path .\EmployeeALMDemo.zip --managed falseReplace EmployeeALMDemo with your solution’s unique name. This ZIP is used to generate a deployment-settings template.
12.3 Generate a deployment-settings template
Create a folder for the settings files, then run:
New-Item -ItemType Directory -Path ".\deployment-settings" -Force
pac solution create-settings --solution-zip ".\EmployeeALMDemo.zip" --settings-file ".\deployment-settings\prod.json"This command creates a settings template from the exported solution. You can use the generated file as the starting point for prod.json, then create qa.json from the same solution template and set the values appropriate for QA.
The generated template is not a guarantee that every value is ready for deployment. Review the file and set the target-specific values before committing it.
12.4 Update the settings for each environment
Review each JSON file and configure:
- Environment-variable values for the target environment.
- Connection-reference mappings to the intended target-environment connections.
- Any other supported deployment settings required by the solution.
Keep qa.json configured for QA resources and prod.json configured for Production resources. Validate that both files are valid JSON.
When do these files need to change? Usually, update them when you add a new environment variable or connection reference to the solution, or when the target environment’s configuration changes. After changing the solution, regenerate or refresh the template as needed, then review and update the target-specific values in both files.
Only commit values that your organization’s policy permits in source control. If a setting contains a credential or other secret, use an approved secret-management approach rather than committing the secret as plain text.
Reference: Power Platform CLI solution commands.
13.Workflow 1 — Export the Dev solution
- File
.github/workflows/export-dev-solution.yml- Purpose
- Capture changes from Dev in source control without directly changing
main.
The workflow is started manually when a set of Dev changes is ready to be captured. When it runs, it:
- Connects to the Dev environment using the configured service principal.
- Reads the current solution version and calculates the next version from the selected option:
revision,build,minor,major, orcustom. - Updates the online solution version in Dev.
- Exports two ZIP files:
EmployeeALMDemo.zip(unmanaged) andEmployeeALMDemo_managed.zip(managed). - Removes the previous source and unpacks both ZIPs into
solutions/with<solution-name>/ pac solution unpack --packagetype Both. - Creates an automation branch, commits the source changes, pushes the branch, and opens a pull request against
main. - Uploads the original exports as troubleshooting artifacts, kept for 7 days.
This workflow uses ALM_GITHUB_TOKEN for the GitHub branch and PR operations. Power Platform authentication uses the PP_APP_ID, PP_TENANT_ID, PP_CLIENT_SECRET, and DEV_ENVIRONMENT_URL secrets.
Workflow code
name: Export Dev Solution
on:
workflow_dispatch:
inputs:
version_type:
description: "Choose how to update the solution version"
required: true
type: choice
default: revision
options:
- revision
- build
- minor
- major
- custom
custom_version:
description: "Custom version (example: 2.3.0.0). Used only for custom."
required: false
type: string
default: ""
permissions:
contents: write
pull-requests: write
env:
SOLUTION_NAME: ${{ vars.PP_SOLUTION_NAME }}
jobs:
export-dev-solution:
name: Export Dev Solution
runs-on: ubuntu-24.04
steps:
# 1. Checkout repository
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
# 2. Install Power Platform tools
- name: Install Power Platform tools
uses: microsoft/powerplatform-actions/actions-install@v1
# 3. Set up .NET for PAC CLI
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
# 4. Install PAC CLI
- name: Install PAC CLI
shell: bash
run: |
set -euo pipefail
dotnet tool update --global Microsoft.PowerApps.CLI.Tool \
|| dotnet tool install --global Microsoft.PowerApps.CLI.Tool
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
"$HOME/.dotnet/tools/pac" help
# 5. Verify Dev connection
- name: Verify Dev connection
uses: microsoft/powerplatform-actions/who-am-i@v1
with:
environment-url: ${{ secrets.DEV_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
# 6. Authenticate PAC CLI to Dev
- name: Authenticate PAC CLI to Dev
shell: bash
env:
DEV_URL: ${{ secrets.DEV_ENVIRONMENT_URL }}
APP_ID: ${{ secrets.PP_APP_ID }}
CLIENT_SECRET: ${{ secrets.PP_CLIENT_SECRET }}
TENANT_ID: ${{ secrets.PP_TENANT_ID }}
run: |
set -euo pipefail
pac auth create \
--name "Dev-SP" \
--url "$DEV_URL" \
--applicationId "$APP_ID" \
--clientSecret "$CLIENT_SECRET" \
--tenant "$TENANT_ID"
pac auth select --name "Dev-SP"
# 7. Calculate next solution version
- name: Calculate solution version
id: version
shell: bash
env:
DEV_URL: ${{ secrets.DEV_ENVIRONMENT_URL }}
VERSION_TYPE: ${{ inputs.version_type }}
CUSTOM_VERSION: ${{ inputs.custom_version }}
run: |
set -euo pipefail
if [ -z "$SOLUTION_NAME" ]; then
echo "::error::PP_SOLUTION_NAME is not configured."
exit 1
fi
OUTPUT=$(pac solution online-version \
--solution-name "$SOLUTION_NAME" \
--environment "$DEV_URL")
echo "$OUTPUT"
CURRENT_VERSION=$(printf '%s\n' "$OUTPUT" |
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' |
head -n 1 || true)
if [[ ! "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Could not determine current solution version."
exit 1
fi
IFS='.' read -r MAJOR MINOR BUILD REVISION <<< "$CURRENT_VERSION"
case "$VERSION_TYPE" in
revision)
REVISION=$((REVISION + 1))
NEW_VERSION="$MAJOR.$MINOR.$BUILD.$REVISION"
;;
build)
BUILD=$((BUILD + 1))
NEW_VERSION="$MAJOR.$MINOR.$BUILD.0"
;;
minor)
MINOR=$((MINOR + 1))
NEW_VERSION="$MAJOR.$MINOR.0.0"
;;
major)
MAJOR=$((MAJOR + 1))
NEW_VERSION="$MAJOR.0.0.0"
;;
custom)
if [[ ! "$CUSTOM_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "::error::Enter a valid four-part version, e.g. 2.3.0.0."
exit 1
fi
NEW_VERSION="$CUSTOM_VERSION"
;;
*)
echo "::error::Unsupported version type: $VERSION_TYPE"
exit 1
;;
esac
IFS='.' read -r V1 V2 V3 V4 <<< "$NEW_VERSION"
for PART in "$V1" "$V2" "$V3" "$V4"; do
if (( PART > 65535 )); then
echo "::error::Version components must not exceed 65535."
exit 1
fi
done
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "::error::New version must differ from current version."
exit 1
fi
echo "Current version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
# 8. Update solution version in Dev
- name: Update solution version in Dev
shell: bash
env:
DEV_URL: ${{ secrets.DEV_ENVIRONMENT_URL }}
NEW_VERSION: ${{ steps.version.outputs.new_version }}
run: |
set -euo pipefail
pac solution online-version \
--solution-name "$SOLUTION_NAME" \
--solution-version "$NEW_VERSION" \
--environment "$DEV_URL"
echo "Updated $SOLUTION_NAME to version $NEW_VERSION."
# 9. Create export directory
- name: Create export directory
shell: bash
run: mkdir -p out
# 10. Export unmanaged solution
- name: Export unmanaged solution
uses: microsoft/powerplatform-actions/export-solution@v1
with:
environment-url: ${{ secrets.DEV_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
solution-name: ${{ env.SOLUTION_NAME }}
solution-output-file: out/${{ env.SOLUTION_NAME }}.zip
managed: false
overwrite: true
run-asynchronously: true
max-async-wait-time: 60
# 11. Export managed solution into the SAME directory.
# PAC CLI Both unpacking uses the matching managed ZIP.
- name: Export managed solution
uses: microsoft/powerplatform-actions/export-solution@v1
with:
environment-url: ${{ secrets.DEV_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
solution-name: ${{ env.SOLUTION_NAME }}
solution-output-file: out/${{ env.SOLUTION_NAME }}_managed.zip
managed: true
overwrite: true
run-asynchronously: true
max-async-wait-time: 60
# 12. Verify both exports and their matching names
- name: Verify exported packages
shell: bash
run: |
set -euo pipefail
UNMANAGED_ZIP="out/${SOLUTION_NAME}.zip"
MANAGED_ZIP="out/${SOLUTION_NAME}_managed.zip"
if [ ! -s "$UNMANAGED_ZIP" ]; then
echo "::error::Unmanaged ZIP missing: $UNMANAGED_ZIP"
exit 1
fi
if [ ! -s "$MANAGED_ZIP" ]; then
echo "::error::Managed ZIP missing: $MANAGED_ZIP"
exit 1
fi
unzip -t "$UNMANAGED_ZIP"
unzip -t "$MANAGED_ZIP"
echo "Both solution exports are present and valid."
ls -lh "$UNMANAGED_ZIP" "$MANAGED_ZIP"
# 13. Unpack BOTH formats into the Git source folder.
# The matching *_managed.zip must remain beside the unmanaged ZIP.
- name: Unpack combined solution source
shell: bash
run: |
set -euo pipefail
UNMANAGED_ZIP="out/${SOLUTION_NAME}.zip"
SOURCE_FOLDER="solutions/${SOLUTION_NAME}"
mkdir -p solutions
# Remove old source so files from an earlier export
# cannot accidentally remain in the new source.
rm -rf "$SOURCE_FOLDER"
pac solution unpack \
--zipfile "$UNMANAGED_ZIP" \
--folder "$SOURCE_FOLDER" \
--packagetype Both \
--clobber
if [ ! -s "$SOURCE_FOLDER/Other/Solution.xml" ]; then
echo "::error::Unpacked Solution.xml was not found."
exit 1
fi
echo "Combined solution source created successfully."
echo "Source folder: $SOURCE_FOLDER"
find "$SOURCE_FOLDER" -type f | wc -l
# 14. Stage the source changes
- name: Stage solution source
shell: bash
run: |
set -euo pipefail
git add "solutions/$SOLUTION_NAME"
if git diff --cached --quiet; then
echo "No solution source changes detected."
echo "Dev version may already have been updated."
exit 0
fi
# 15. Push export branch and create PR
- name: Push export branch and create pull request
shell: bash
env:
ALM_GITHUB_TOKEN: ${{ secrets.ALM_GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.ALM_GITHUB_TOKEN }}
NEW_VERSION: ${{ steps.version.outputs.new_version }}
CURRENT_VERSION: ${{ steps.version.outputs.current_version }}
VERSION_TYPE: ${{ inputs.version_type }}
run: |
set -euo pipefail
if [ -z "$ALM_GITHUB_TOKEN" ]; then
echo "::error::ALM_GITHUB_TOKEN is not configured."
exit 1
fi
# Skip creating an empty PR.
if git diff --cached --quiet; then
echo "No staged changes. Skipping PR creation."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email \
"41898282+github-actions[bot]@users.noreply.github.com"
BRANCH_NAME="automation/export-dev-${GITHUB_RUN_ID}"
git checkout -b "$BRANCH_NAME"
git commit \
-m "Export $SOLUTION_NAME from Dev ($NEW_VERSION)"
git remote set-url origin \
"https://x-access-token:${ALM_GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push --set-upstream origin "$BRANCH_NAME"
PR_URL=$(gh pr create \
--repo "$GITHUB_REPOSITORY" \
--base main \
--head "$BRANCH_NAME" \
--title "Export $SOLUTION_NAME from Dev - v$NEW_VERSION" \
--body "## Summary
- Solution: $SOLUTION_NAME
- Previous version: $CURRENT_VERSION
- New version: $NEW_VERSION
- Version update type: $VERSION_TYPE
- Exported managed and unmanaged packages from Dev.
- Unpacked both formats into the source folder for Git.
## Validation
Review the source changes and ensure required checks pass before merging.
The managed build workflow runs after the source is merged to main.")
echo "Pull request created: $PR_URL"
# 16. Upload exported ZIPs for troubleshooting
- name: Upload exported packages
if: always()
uses: actions/upload-artifact@v6
with:
name: ${{ env.SOLUTION_NAME }}-dev-exports-${{ github.run_id }}
path: out/*.zip
if-no-files-found: ignore
retention-days: 7After the workflow completes, review the PR’s source diff. Confirm that the changes are the ones you intended to capture from Dev.
14.Workflow 2 — Build and Check
- File
.github/workflows/build-and-check.yml- Purpose
- Validate proposed changes before they are merged into
main.
The workflow runs on pull requests targeting main. Depending on the final YAML, it should:
- Check out the proposed source.
- Install the required Power Platform tooling.
- Confirm that the solution source folder exists.
- Validate
deployment-settings/andqa.json deployment-settings/.prod.json - Pack the unmanaged solution from source.
- Run Power Platform Solution Checker.
- Publish diagnostics or other check results when available.
The validation status is used by branch protection to decide whether the PR can be merged. In the POC, the ruleset protects main and requires the check named Validate Power Platform Solution. Because the POC is maintained by one person, it uses zero required human approvals.
A successful validation workflow does not automatically guarantee that the solution will import successfully into every target environment. Deployment and environment-specific testing remain necessary.
Workflow code
name: Build and Check
on:
pull_request:
branches:
- main
workflow_dispatch:
permissions:
contents: read
security-events: write
jobs:
validate:
name: Validate Power Platform Solution
runs-on: ubuntu-latest
steps:
# 1. Checkout source code
- name: Checkout repository
uses: actions/checkout@v4
# 2. Install Power Platform tools
- name: Install Power Platform Tools
uses: microsoft/powerplatform-actions/actions-install@v1
# 3. Validate solution folder
- name: Validate solution folder
shell: bash
run: |
set -euo pipefail
SOLUTION_FOLDER="solutions/EmployeeALMDemo"
if [ ! -d "$SOLUTION_FOLDER" ]; then
echo "::error::Solution folder not found: $SOLUTION_FOLDER"
exit 1
fi
echo "Solution source folder found."
# 4. Validate deployment settings JSON
- name: Validate deployment settings
shell: bash
run: |
set -euo pipefail
python - <<'PY'
import json
from pathlib import Path
files = [
Path("deployment-settings/qa.json"),
Path("deployment-settings/prod.json"),
]
for file in files:
if not file.is_file():
raise SystemExit(f"Missing file: {file}")
with file.open(encoding="utf-8") as stream:
json.load(stream)
print(f"Valid JSON: {file}")
PY
# 5. Create output directory
- name: Prepare output directory
shell: bash
run: mkdir -p out
# 6. Pack solution
- name: Pack Power Platform Solution
uses: microsoft/powerplatform-actions/pack-solution@v1
with:
solution-folder: solutions/EmployeeALMDemo
solution-file: out/EmployeeALMDemo.zip
solution-type: Unmanaged
# 7. Run Power Platform Solution Checker
- name: Run Power Platform Solution Checker
uses: microsoft/powerplatform-actions/check-solution@v1
with:
environment-url: ${{ secrets.DEV_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
path: out/EmployeeALMDemo.zip
geo: India
fail-on-analysis-error: true
checker-logs-artifact-name: solution-checker-logs
# 8. Upload generated files for troubleshooting
- name: Upload Solution Checker results
if: always()
uses: actions/upload-artifact@v4
with:
name: power-platform-solution-checker-results
path: out/
if-no-files-found: warnA successful pack step and a successful Solution Checker run are different signals. Review the complete workflow result and its diagnostics.
15.Workflow 3 — Build the managed solution
- File
.github/workflows/build-managed-solution.yml- Purpose
- Produce the managed solution package from the reviewed GitHub source.
This is the key difference between source-based ALM and simply deploying the ZIP exported from Dev. The workflow runs when solution source is merged into main, and can also be started manually. It:
- Checks out the merged Git source.
- Validates the solution folder and its
Solution.xmlmetadata. - Uses PAC CLI to pack the source as a managed solution.
- Checks that the output ZIP exists and is valid.
- Uploads the resulting ZIP as a GitHub Actions artifact.
The artifact used by this POC:
Artifact: managed-solution
ZIP: EmployeeALMDemo_managed.zip
Retention: 30 daysThe artifact name is important because the QA and Production workflows download it by that name. Retention is controlled by the workflow’s artifact-upload configuration and repository policy.
Packing as Managed only works when the source was unpacked with both package types. If the source contains only unmanaged files, the managed pack fails, which is why the export workflow uses --packagetype Both.
Workflow code
name: Build Managed Solution
on:
push:
branches:
- main
paths:
- "solutions/**"
- ".github/workflows/build-managed-solution.yml"
workflow_dispatch:
permissions:
contents: read
env:
SOLUTION_NAME: ${{ vars.PP_SOLUTION_NAME }}
jobs:
build-managed-solution:
name: Build Managed Solution
runs-on: ubuntu-24.04
steps:
# 1. Checkout the merged solution source
- name: Checkout repository
uses: actions/checkout@v5
# 2. Validate the solution source without assuming
# that Solution.xml must contain Managed=0
- name: Validate solution source
shell: bash
run: |
set -euo pipefail
if [ -z "$SOLUTION_NAME" ]; then
echo "::error::PP_SOLUTION_NAME is not configured."
exit 1
fi
SOLUTION_FOLDER="solutions/$SOLUTION_NAME"
SOLUTION_XML="$SOLUTION_FOLDER/Other/Solution.xml"
if [ ! -d "$SOLUTION_FOLDER" ]; then
echo "::error::Solution folder not found: $SOLUTION_FOLDER"
exit 1
fi
if [ ! -s "$SOLUTION_XML" ]; then
echo "::error::Solution metadata not found: $SOLUTION_XML"
exit 1
fi
echo "Solution folder found: $SOLUTION_FOLDER"
echo "Solution metadata found: $SOLUTION_XML"
echo "Managed metadata values, if present:"
grep -n -E '<Managed>[[:space:]]*[01][[:space:]]*</Managed>' \
"$SOLUTION_XML" || true
echo "Source file count:"
find "$SOLUTION_FOLDER" -type f | wc -l
echo "Solution source validation passed."
# 3. Install Power Platform tools
- name: Install Power Platform tools
uses: microsoft/powerplatform-actions/actions-install@v1
# 4. Create artifact directory
- name: Create artifact directory
shell: bash
run: mkdir -p artifacts
# 5. Pack the committed source as Managed
- name: Pack managed solution
id: pack
uses: microsoft/powerplatform-actions/pack-solution@v1
with:
solution-folder: solutions/${{ env.SOLUTION_NAME }}
solution-file: artifacts/${{ env.SOLUTION_NAME }}_managed.zip
solution-type: Managed
# 6. Verify the generated ZIP
- name: Verify managed solution package
shell: bash
run: |
set -euo pipefail
PACKAGE="artifacts/${SOLUTION_NAME}_managed.zip"
if [ ! -s "$PACKAGE" ]; then
echo "::error::Managed package was not created or is empty."
exit 1
fi
echo "Checking ZIP integrity..."
unzip -t "$PACKAGE"
echo "Checking package contents..."
unzip -Z1 "$PACKAGE" | head -n 30
echo "Managed package created successfully."
ls -lh "$PACKAGE"
# 7. Upload artifact for QA and Production
- name: Upload managed solution artifact
uses: actions/upload-artifact@v6
with:
name: managed-solution
path: artifacts/${{ env.SOLUTION_NAME }}_managed.zip
if-no-files-found: error
retention-days: 30The important release principle is that the managed artifact should be built from the reviewed source in GitHub. This makes the build traceable to the commit that passed PR validation.
16.Workflow 4 — Deploy to QA
- File
.github/workflows/deploy-qa.yml- Purpose
- Deploy a selected managed build to QA without rebuilding it.
The workflow is started manually and accepts an optional build_run_id. If no run ID is supplied, it uses the latest successful Build Managed Solution run on main. It then:
- Validates the repository configuration and
deployment-settings/.qa.json - Confirms that the selected build has a
managed-solutionartifact that has not expired. - Downloads the artifact from that specific build run.
- Checks that
EmployeeALMDemo_managed.zipexists and is not empty. - Confirms the connection to QA.
- Imports the managed solution using
deployment-settings/for environment-specific connection references and environment variables.qa.json - Publishes changes and waits for the asynchronous import according to the configured timeout.
The important part is that QA downloads the build artifact rather than exporting a fresh package from Dev.
Workflow code
name: Deploy to QA
on:
workflow_dispatch:
inputs:
build_run_id:
description: 'Optional: GitHub Actions run ID that created the managed artifact'
required: false
type: string
permissions:
contents: read
actions: read
env:
SOLUTION_NAME: ${{ vars.PP_SOLUTION_NAME }}
ARTIFACT_NAME: managed-solution
SOLUTION_ZIP: out/${{ vars.PP_SOLUTION_NAME }}_managed.zip
jobs:
deploy-qa:
name: Import Managed Solution into QA
runs-on: ubuntu-24.04
environment: QA
steps:
# 1. Checkout repository
- name: Checkout repository
uses: actions/checkout@v5
# 2. Validate configuration and deployment settings
- name: Validate configuration
shell: bash
env:
QA_URL: ${{ secrets.QA_ENVIRONMENT_URL }}
APP_ID: ${{ secrets.PP_APP_ID }}
TENANT_ID: ${{ secrets.PP_TENANT_ID }}
CLIENT_SECRET: ${{ secrets.PP_CLIENT_SECRET }}
run: |
set -euo pipefail
if [ -z "$SOLUTION_NAME" ]; then
echo "::error::Repository variable PP_SOLUTION_NAME is missing."
exit 1
fi
if [ -z "$QA_URL" ] || [ -z "$APP_ID" ] || \
[ -z "$TENANT_ID" ] || [ -z "$CLIENT_SECRET" ]; then
echo "::error::One or more required GitHub secrets are missing."
exit 1
fi
if [ ! -f deployment-settings/qa.json ]; then
echo "::error::deployment-settings/qa.json not found."
exit 1
fi
python -m json.tool deployment-settings/qa.json > /dev/null
echo "Configuration validation passed."
echo "Solution: $SOLUTION_NAME"
echo "Expected artifact: $ARTIFACT_NAME"
# 3. Select the successful managed solution build
- name: Select managed solution build
id: select-build
shell: bash
env:
GH_TOKEN: ${{ github.token }}
INPUT_RUN_ID: ${{ inputs.build_run_id }}
run: |
set -euo pipefail
if [ -n "$INPUT_RUN_ID" ]; then
if [[ ! "$INPUT_RUN_ID" =~ ^[0-9]+$ ]]; then
echo "::error::Build run ID must contain digits only."
exit 1
fi
RUN_ID="$INPUT_RUN_ID"
echo "Using manually selected build run: $RUN_ID"
else
RUN_ID=$(gh run list \
--repo "$GITHUB_REPOSITORY" \
--workflow build-managed-solution.yml \
--branch main \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId // empty')
if [ -z "$RUN_ID" ]; then
echo "::error::No successful Build Managed Solution run found on main."
echo "Run the build workflow successfully before deploying to QA."
exit 1
fi
echo "Selected latest successful build run: $RUN_ID"
fi
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
# 4. Verify that the selected run contains the expected artifact
- name: Verify build artifact exists
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUILD_RUN_ID: ${{ steps.select-build.outputs.run_id }}
run: |
set -euo pipefail
echo "Checking artifact '$ARTIFACT_NAME' in run '$BUILD_RUN_ID'..."
ARTIFACT_INFO=$(gh api \
"repos/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN_ID/artifacts" \
--jq ".artifacts[] | select(.name == \"$ARTIFACT_NAME\" and .expired == false) | {name: .name, expired: .expired, size_in_bytes: .size_in_bytes}" \
| head -n 1 || true)
if [ -z "$ARTIFACT_INFO" ]; then
echo "::error::Artifact '$ARTIFACT_NAME' was not found in build run $BUILD_RUN_ID."
echo "Open that run in GitHub Actions and check its Artifacts section."
echo "Confirm that the build completed its Upload managed solution artifact step."
exit 1
fi
echo "$ARTIFACT_INFO"
echo "Artifact exists and is not expired."
# 5. Download the managed solution from the selected build run
- name: Download managed solution artifact
uses: actions/download-artifact@v8
with:
name: ${{ env.ARTIFACT_NAME }}
path: out
repository: ${{ github.repository }}
run-id: ${{ steps.select-build.outputs.run_id }}
github-token: ${{ github.token }}
# 6. Verify the downloaded managed solution ZIP
- name: Verify managed solution ZIP
shell: bash
run: |
set -euo pipefail
PACKAGE="$SOLUTION_ZIP"
if [ ! -s "$PACKAGE" ]; then
echo "::error::Expected managed solution ZIP not found: $PACKAGE"
echo "Downloaded files:"
find out -maxdepth 2 -type f -print || true
exit 1
fi
echo "Checking ZIP integrity..."
unzip -t "$PACKAGE"
echo "Managed solution artifact verified:"
ls -lh "$PACKAGE"
# 7. Install Power Platform tools
- name: Install Power Platform tools
uses: microsoft/powerplatform-actions/actions-install@v1
# 8. Verify connection to QA
- name: Verify QA environment connection
uses: microsoft/powerplatform-actions/who-am-i@v1
with:
environment-url: ${{ secrets.QA_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
# 9. Import the managed solution into QA
- name: Import managed solution into QA
uses: microsoft/powerplatform-actions/import-solution@v1
with:
environment-url: ${{ secrets.QA_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
solution-file: ${{ env.SOLUTION_ZIP }}
use-deployment-settings-file: true
deployment-settings-file: deployment-settings/qa.json
publish-changes: true
run-asynchronously: true
max-async-wait-time: 60After deployment, test the solution in the QA environment and record the build run ID and test outcome. QA settings should refer to QA connections and environment-specific values.
17.Workflow 5 — Deploy to Production
- File
.github/workflows/deploy-prod.yml- Purpose
- Deploy the exact managed build that passed QA testing.
The Production workflow follows the same pattern as QA, with an extra release-control step. It has two jobs.
Find Build to Deploy runs before anyone is asked to approve:
- Validates the repository variable and the Production secrets.
- Uses the
build_run_idyou enter, or the latest successful Build Managed Solution run onmainif the input is left blank. - Confirms that the run is a completed, successful Build Managed Solution run on
main. - Confirms that its
managed-solutionartifact is still available. - Writes a build review summary (run ID, commit, and run link) for the approver.
Deploy Managed Solution to Production runs after approval:
- Waits for the required approval on the GitHub
Productionenvironment. - Validates
deployment-settings/.prod.json - Downloads the
managed-solutionartifact from the selected run and verifies the ZIP. - Imports the managed solution using
deployment-settings/, publishes changes, and waits for the import.prod.json - Writes a deployment summary.
Workflow code
name: Deploy to Production
on:
workflow_dispatch:
inputs:
build_run_id:
description: 'Optional: Enter a specific successful Build Managed Solution run ID. Leave blank to automatically select the latest successful build from main.'
required: false
type: string
permissions:
contents: read
actions: read
env:
SOLUTION_NAME: ${{ vars.PP_SOLUTION_NAME }}
ARTIFACT_NAME: managed-solution
SOLUTION_ZIP: out/${{ vars.PP_SOLUTION_NAME }}_managed.zip
jobs:
resolve-build:
name: Find Build to Deploy
runs-on: ubuntu-latest
outputs:
build_run_id: ${{ steps.resolve.outputs.build_run_id }}
steps:
- name: Validate repository configuration
shell: bash
env:
PP_APP_ID: ${{ secrets.PP_APP_ID }}
PP_TENANT_ID: ${{ secrets.PP_TENANT_ID }}
PP_CLIENT_SECRET: ${{ secrets.PP_CLIENT_SECRET }}
PROD_ENVIRONMENT_URL: ${{ secrets.PROD_ENVIRONMENT_URL }}
run: |
set -euo pipefail
if [[ -z "${SOLUTION_NAME:-}" ]]; then
echo "::error::Repository variable PP_SOLUTION_NAME is not configured."
exit 1
fi
if [[ -z "$PP_APP_ID" || -z "$PP_TENANT_ID" ||
-z "$PP_CLIENT_SECRET" || -z "$PROD_ENVIRONMENT_URL" ]]; then
echo "::error::One or more required Power Platform secrets are missing."
exit 1
fi
- name: Resolve Build Run ID
id: resolve
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_BUILD_RUN_ID: ${{ inputs.build_run_id }}
shell: bash
run: |
set -euo pipefail
if [[ -n "$REQUESTED_BUILD_RUN_ID" ]]; then
if [[ ! "$REQUESTED_BUILD_RUN_ID" =~ ^[0-9]+$ ]]; then
echo "::error::Build Run ID must contain only numbers."
exit 1
fi
BUILD_RUN_ID="$REQUESTED_BUILD_RUN_ID"
echo "Using specified Build Run ID: $BUILD_RUN_ID"
else
echo "Finding the latest successful Build Managed Solution run on main..."
BUILD_RUN_ID=$(gh run list \
--repo "$GITHUB_REPOSITORY" \
--workflow "build-managed-solution.yml" \
--branch main \
--status success \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId // empty')
if [[ -z "$BUILD_RUN_ID" ]]; then
echo "::error::No successful Build Managed Solution run was found on main."
exit 1
fi
echo "Automatically selected Build Run ID: $BUILD_RUN_ID"
fi
RUN_DETAILS=$(gh run view "$BUILD_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--json databaseId,workflowName,status,conclusion,headBranch,headSha,url)
echo "$RUN_DETAILS"
WORKFLOW_NAME=$(echo "$RUN_DETAILS" | jq -r '.workflowName')
RUN_STATUS=$(echo "$RUN_DETAILS" | jq -r '.status')
RUN_CONCLUSION=$(echo "$RUN_DETAILS" | jq -r '.conclusion')
RUN_BRANCH=$(echo "$RUN_DETAILS" | jq -r '.headBranch')
RUN_URL=$(echo "$RUN_DETAILS" | jq -r '.url')
RUN_SHA=$(echo "$RUN_DETAILS" | jq -r '.headSha')
if [[ "$WORKFLOW_NAME" != "Build Managed Solution" ]]; then
echo "::error::Selected run is not the Build Managed Solution workflow."
exit 1
fi
if [[ "$RUN_STATUS" != "completed" ||
"$RUN_CONCLUSION" != "success" ||
"$RUN_BRANCH" != "main" ]]; then
echo "::error::The selected build must be completed successfully on main."
exit 1
fi
ARTIFACTS=$(gh api \
"repos/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN_ID/artifacts")
ARTIFACT_COUNT=$(echo "$ARTIFACTS" | jq \
--arg name "managed-solution" \
'[.artifacts[] | select(.name == $name and .expired == false)] | length')
if [[ "$ARTIFACT_COUNT" -lt 1 ]]; then
echo "::error::The selected build has no available managed-solution artifact."
exit 1
fi
echo "build_run_id=$BUILD_RUN_ID" >> "$GITHUB_OUTPUT"
{
echo "## Production Deployment - Build Review"
echo ""
echo "- **Solution:** $SOLUTION_NAME"
echo "- **Build Run ID:** $BUILD_RUN_ID"
echo "- **Source branch:** $RUN_BRANCH"
echo "- **Commit:** \`$RUN_SHA\`"
echo "- **Build status:** $RUN_CONCLUSION"
echo "- **Artifact:** managed-solution"
echo "- **Build run:** $RUN_URL"
echo ""
echo "Review the selected build before approving Production deployment."
} >> "$GITHUB_STEP_SUMMARY"
deploy-prod:
name: Deploy Managed Solution to Production
runs-on: windows-latest
needs: resolve-build
environment: Production
concurrency:
group: production-deployment
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Display selected build
shell: pwsh
env:
BUILD_RUN_ID: ${{ needs.resolve-build.outputs.build_run_id }}
run: |
Write-Host "Selected Build Run ID: $env:BUILD_RUN_ID"
Write-Host "Artifact: managed-solution"
- name: Validate Production configuration
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($env:SOLUTION_NAME)) {
throw "Repository variable PP_SOLUTION_NAME is not configured."
}
$settingsFile = "deployment-settings/prod.json"
if (!(Test-Path $settingsFile)) {
throw "Production deployment settings not found: $settingsFile"
}
$settings = Get-Content $settingsFile -Raw | ConvertFrom-Json
if ($null -eq $settings) {
throw "Production deployment settings are empty or invalid."
}
Write-Host "Production deployment configuration validated."
- name: Download selected managed solution artifact
uses: actions/download-artifact@v8
with:
name: managed-solution
path: out
repository: ${{ github.repository }}
run-id: ${{ needs.resolve-build.outputs.build_run_id }}
github-token: ${{ github.token }}
- name: Verify downloaded solution ZIP
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$zipPath = $env:SOLUTION_ZIP
if (!(Test-Path $zipPath)) {
throw "Expected managed solution ZIP not found: $zipPath"
}
$file = Get-Item $zipPath
if ($file.Length -le 0) {
throw "Managed solution ZIP is empty."
}
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead(
(Resolve-Path $zipPath).Path
)
try {
if ($archive.Entries.Count -eq 0) {
throw "Managed solution ZIP contains no files."
}
}
finally {
$archive.Dispose()
}
Write-Host "Managed solution ZIP verified."
Write-Host "File: $zipPath"
Write-Host "Size: $($file.Length) bytes"
# Initializes the tooling required by Power Platform Actions.
- name: Install Power Platform Tools
uses: microsoft/powerplatform-actions/actions-install@v1
# This action authenticates using the supplied service principal.
- name: Import managed solution into Production
uses: microsoft/powerplatform-actions/import-solution@v1
with:
environment-url: ${{ secrets.PROD_ENVIRONMENT_URL }}
app-id: ${{ secrets.PP_APP_ID }}
client-secret: ${{ secrets.PP_CLIENT_SECRET }}
tenant-id: ${{ secrets.PP_TENANT_ID }}
solution-file: ${{ env.SOLUTION_ZIP }}
use-deployment-settings-file: true
deployment-settings-file: deployment-settings/prod.json
publish-changes: true
run-asynchronously: true
max-async-wait-time: 60
- name: Deployment summary
if: success()
shell: bash
env:
BUILD_RUN_ID: ${{ needs.resolve-build.outputs.build_run_id }}
run: |
{
echo "## Production Deployment Successful"
echo ""
echo "- **Solution:** $SOLUTION_NAME"
echo "- **Build Run ID:** $BUILD_RUN_ID"
echo "- **Artifact:** managed-solution"
echo "- **Environment:** Production"
echo ""
echo "The managed solution was deployed from the selected build artifact."
} >> "$GITHUB_STEP_SUMMARY"The build run ID is optional by design. Leave it blank and the workflow automatically fetches the latest successful build from main, which is the quickest path when nothing has been merged since the QA deployment. If newer builds have been created since QA testing, enter the run ID that QA actually tested. Either way, the approver can confirm the selected build in the build review summary before approving. The same artifact should move from QA to Production; only the target environment’s deployment settings differ.
18.Workflow 6 — CodeQL analysis
- File
.github/workflows/codeql.yml- Purpose
- Scan GitHub Actions workflow files for supported security issues.
This repository primarily contains Power Platform solution files, deployment settings, and GitHub Actions workflows. The current CodeQL configuration therefore analyzes the actions language only. It does not attempt JavaScript/TypeScript or C# analysis. If you later add PCF controls, JavaScript/TypeScript projects, or C# plugins, update the CodeQL configuration to include those languages and ensure the repository contains source code for them.
The workflow runs on pushes to main, pull requests targeting main, a weekly schedule, and manual dispatch. The schedule 30 5 * * 1 means Monday at 05:30 UTC (11:00 AM India Standard Time).
Workflow code
name: CodeQL Security Analysis
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: "30 5 * * 1"
workflow_dispatch:
permissions:
contents: read
security-events: write
actions: read
jobs:
analyze:
name: Analyze GitHub Actions
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: actions
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4Review the CodeQL workflow run in GitHub → Actions → CodeQL Security Analysis. A successful run confirms that the configured Actions analysis completed; it does not replace Power Platform Solution Checker or validate the business behavior of the solution. If GitHub Code Security’s default setup is also enabled, check whether it runs a separate analysis configuration.
19.End-to-end release process
Once the repository and workflows are configured, use this process for each release.
A. Develop and export
- Make the required changes in Dev.
- Test the changes in Dev.
- Run
export-dev-solution.yml. - Review the generated export branch and pull request.
- Confirm the unpacked source changes match the intended solution updates.
B. Validate and merge
- Wait for
build-and-check.ymlto complete on the PR. - Review the Solution Checker results and any validation output.
- Resolve issues and update the PR as needed.
- Merge only when the required checks pass and the review process is complete.
C. Build the release artifact
- Run
build-managed-solution.yml, or let the configuredmaintrigger run. - Confirm that the
managed-solutionartifact was uploaded. - Record the build run ID and source revision.
D. Deploy and test in QA
- Run
deploy-qa.yml. - Select the intended managed build according to the workflow’s inputs.
- Confirm that the solution imports successfully into QA.
- Run functional and integration tests for the solution.
- Record the build run ID and QA test outcome.
E. Promote to Production
- Run
deploy-prod.yml. - Leave the build run ID blank to use the latest successful build, or enter the run ID that passed QA if newer builds exist.
- Obtain the required Production environment approval.
- Confirm that the managed solution imports successfully.
- Perform Production smoke tests and record the release result.
20.Final validation checklist
Use this checklist after initial setup and during the first complete release.
Environment and authentication6 items
GitHub configuration7 items
Source, settings, and build9 items
QA and Production release9 items
21.Troubleshooting workflow failures
When a workflow fails, open the specific GitHub Actions run and identify the failed job and step. Use the step logs to distinguish authentication, permission, source validation, packaging, artifact, and import problems.
Typical areas to inspect include:
- Authentication: environment URL, tenant ID, client ID, client secret validity, and application-user access.
- GitHub branch/PR creation: PAT expiry, repository scope, Contents permission, and Pull requests permission.
- PR validation: solution folder path, workflow trigger, JSON syntax, packaging output, and Solution Checker logs.
- Artifact build: source path, solution unique name, whether the source was unpacked with both package types, managed packaging, and the
managed-solutionartifact name. - QA/Production import: selected build run ID, artifact contents, deployment settings, connection references, environment variables, dependencies, and target-environment permissions.
Correct the underlying issue and rerun the appropriate workflow. Keep the QA-tested build identifier for the Production release.
22.Recommended official documentation
- Microsoft Power Platform GitHub Actions deployment tutoriallearn.microsoft.com
- Available Power Platform GitHub Actionslearn.microsoft.com
- Register an application for Dataverselearn.microsoft.com
- Power Platform CLI solution commandslearn.microsoft.com
- GitHub protected branchesdocs.github.com
- GitHub Actions deployment environments and approvalsdocs.github.com
Conclusion
This setup establishes a practical ALM lifecycle for Power Platform: export Dev changes into source control, review and validate them through pull requests, build a managed solution from the reviewed source, deploy that artifact to QA, and promote the same tested artifact to Production after approval.
The key to repeatable releases is keeping the responsibilities clear: Dev is where changes are made, GitHub is where source changes are reviewed, the managed artifact is what gets deployed, deployment settings provide target-specific configuration, and QA evidence plus Production approval control the final release.
Start with the sample EmployeeALMDemo solution as a POC, replace the example identifiers and dummy environment URLs with your own values, and test each workflow in your own tenant before relying on it for releases.