Proper Power Platform ALM: Moving from Manual ZIP Files to PAC CLI and Pipelines

A working reference for the Power Platform CLI solution commands, and the migration path off hand carried solution ZIP files: what export, import, pack, unpack, clone, sync, version, check and create-settings each do, which of them belong on a laptop and which belong only on a build agent, and how to get an environment that already has years of direct changes onto a reviewable release path, then how to standardise that release path across many environments and many clients.

Stop moving Power Platform and Dynamics 365 changes by exporting a solution ZIP and importing it by hand. Proper application lifecycle management uses the Power Platform CLI, invoked as pac. Four commands carry most of the work: pac solution export pulls a solution out of an environment as a ZIP, pac solution unpack explodes that ZIP into readable files you commit to Git, pac solution pack rebuilds a ZIP from those committed files, and pac solution import pushes it into the next environment. Development stays unmanaged, test and production only ever receive managed solutions, and a pipeline runs the pack and import steps so no human ever carries a file between environments again.

Two notes before the commands. First, the Power Platform CLI is versioned and it changes often: flags get added, long forms get short aliases, defaults shift, and a few commands that were preview when a tutorial was written are now general availability with different names. Treat every command below as the shape of the operation rather than as a string to paste, and confirm the exact flags for your installed version with pac help solution and with pac solution export --help before you put anything in a pipeline. Second, this page is about the operation rather than the installation. The CLI ships in three forms, as a dotnet tool, as a Visual Studio Code extension, and as a standalone Windows install, and which one you have decides how you upgrade it but changes nothing about the commands themselves.

Why move from manual export and import to the PAC CLI?

The ZIP is a build output, not the thing you are keeping

A solution ZIP is one opaque binary that a diff tool cannot read, a reviewer cannot review, and a merge cannot merge. pac solution unpack turns it into a directory tree of XML, JavaScript, plugin assemblies and canvas app sources, one file per component, and that tree is what belongs in your repository. From then on the ZIP is regenerated by pac solution pack whenever you need one, in the same way you would never commit a compiled binary and then edit it.

Export pairs with unpack, pack pairs with import

Those two pairings are the whole mental model. Coming out of an environment you run export then unpack, which gets platform changes into source control. Going into an environment you run pack then import, which gets committed source into a target. Once the repository exists, pac solution clone and pac solution sync collapse each pair into a single command, and once a pipeline exists, nobody runs the second pair by hand at all.

Unmanaged in development, managed everywhere else, and never both

A change is made in an unmanaged solution in development, and it reaches test and production only as a managed solution imported by a pipeline. That single rule is what makes a change reversible, because a managed component can be uninstalled and an unmanaged one cannot. Environments that receive unmanaged imports accumulate layers nobody can remove, which is the condition most of the environments we are asked to rescue are actually in.

Which pac solution commands do you run, and in what order?

Grouped by the sequence rather than alphabetically, because the sequence is the part a command reference cannot give you. Flag names and defaults move between CLI releases, so confirm each one against your installed version with pac help solution before it goes into a pipeline.

CommandWhat it doesWhen you run it
pac auth createCreates a stored authentication profile against one environment, interactively for a person or with an application id, client secret and tenant for a build agent.Once per environment on a workstation, and once per run on a build agent using a service principal rather than a named user.
pac auth list / pac auth selectLists the stored profiles and switches the active one. pac org who confirms which environment and which user you are currently acting as.Before every command that writes. This is the cheapest habit on the page and it is the one that prevents importing into the wrong environment.
pac solution listLists the solutions installed in the connected environment with their unique names and versions.At the start of any takeover, to find out what the environment actually contains rather than what you were told it contains.
pac solution exportDownloads a solution from the connected environment as a ZIP. Takes the solution unique name, an output path, and whether the artifact is managed or unmanaged.Whenever changes made in the maker portal need to come back into source control. Always exported unmanaged from development.
pac solution unpackExplodes a solution ZIP into a folder of individual component files. The processCanvasApps option additionally unpacks canvas app sources so they are diffable rather than a single binary.Immediately after every export. The unpacked folder, not the ZIP, is what you commit.
pac solution packThe inverse of unpack. Rebuilds a solution ZIP from an unpacked folder, as managed or unmanaged, so the artifact is produced from committed source rather than from somebody workstation.In the build stage of the pipeline, every time, and almost never by hand.
pac solution importImports a solution ZIP into the connected environment, with options to publish customizations, activate plugins, run asynchronously, force an overwrite of unmanaged customizations, and apply a deployment settings file.In the release stage of the pipeline. Managed only, into test and then production, and never as a manual step by a person with a browser.
pac solution cloneExport and unpack in one command, and it also creates the project files so the folder can be built as a repeatable artifact.Once, at the start, to take the baseline of a solution that already exists in an environment and has never been in source control.
pac solution syncRefreshes an already cloned local folder with the current state of the environment, so the change set appears as a diff in your working tree.Every working day, on a branch, after somebody has made changes in the development environment. This is the command that replaces routine exporting.
pac solution init / pac solution add-referenceCreates a new solution project with a publisher name and prefix, and adds a reference to a code project such as a plugin or a PCF control so the build produces one artifact containing both.When starting a new solution, or when code components need to ship inside the same solution as the configuration that uses them.
pac solution versionIncrements the solution version in the project, so the build number is stamped in one place rather than typed into a form by whoever is deploying.In the build stage, before pack. An unincremented version is the most common cause of an import that succeeds and changes nothing.
pac solution checkRuns the Power Platform solution checker against a packed artifact and writes the results out, so static analysis findings can fail a build.In the build stage, after pack and before anything touches an environment. Set the severity that fails the build and agree it with the team once.
pac solution create-settingsGenerates a deployment settings file listing every environment variable and connection reference the solution contains, ready to be filled in per environment.Once per solution, then again whenever a new environment variable or connection reference is added. Pass the filled file to import.
pac solution upgradeApplies a staged solution upgrade, which is the path that removes components deleted since the previous version rather than leaving them behind.When a release deletes components. A plain import updates and adds but does not remove, which is how orphaned columns and views accumulate.
pac data export / pac data importMoves configuration data, meaning reference records rather than transactional records, between environments using a schema file that names the tables and columns.Whenever a solution depends on records rather than only on metadata. Configuration data belongs in the pipeline next to the solution, not in a spreadsheet.

Authenticate once per environment, and check before you write

The profiles are stored on the machine and persist between sessions, which is convenient and is also exactly why the active one needs checking. The worst accident available in this stack is an import that runs against production while the person running it believes they are pointed at a sandbox.

Authentication profiles
# One named profile per environment. You will have several, so name them.
pac auth create --name dev  --environment https://contoso-dev.crm4.dynamics.com
pac auth create --name test --environment https://contoso-test.crm4.dynamics.com
pac auth create --name prod --environment https://contoso.crm4.dynamics.com

# Confirm which one you are pointed at before every command that writes.
pac auth list
pac auth select --name dev
pac org who

Take the baseline: export and unpack, or clone

This is the one time operation that turns a solution which has only ever existed inside an environment into source you can read, review and merge. Run it against development, never against production, and commit the unpacked folder rather than the ZIP.

First move: the solution into source control
# One time only. Take the solution that already lives in development
# and put its source, file by file, into the repository.
pac auth select --name dev
pac solution list

# The explicit pair, which is worth running once so you can see both halves.
pac solution export --name ContosoSales \
  --path ./artifacts/ContosoSales.zip --managed false
pac solution unpack --zipfile ./artifacts/ContosoSales.zip \
  --folder ./src/ContosoSales --packagetype Unmanaged --processCanvasApps

# Or the same thing in one command, which also writes the project files.
pac solution clone --name ContosoSales --outputDirectory ./src

git add src/ContosoSales
git commit -m "Baseline: ContosoSales exactly as development holds it today"

The daily loop: sync onto a branch and read the diff

Makers keep working in the maker portal exactly as before. What changes is that their work becomes a reviewable change set on a branch instead of an invisible edit to a live environment, and this is the point where the method starts paying for itself rather than costing.

Every working day
# Somebody changed things in the development environment this week.
# Pull them onto a branch and read the diff before you commit anything.
git checkout -b feature/quote-approval
pac solution sync --solution-folder ./src/ContosoSales
git diff

# The diff is the code review. A column nobody asked for, a form section added
# during a demo, a flow left switched on by a developer: all of it is visible
# here, which is the point. It stops on the branch rather than in production.
git add -A
git commit -m "Quote approval: two columns, a form section and the approval flow"

The pipeline: pack from committed source, check, then import managed

Written as raw pac commands rather than as tasks, because the tasks in Azure DevOps and the actions in GitHub wrap these same operations and it is worth seeing what they are doing. Note the two things a person can no longer get wrong here: the version is stamped by the build, and only the managed artifact travels.

Build and release stages
# Build agent. No person, no browser, no ZIP file on anybody laptop.
pac auth create --name build \
  --environment https://contoso-build.crm4.dynamics.com \
  --applicationId $APP_ID --clientSecret $CLIENT_SECRET --tenant $TENANT_ID

# Stamp the build number on, then produce both artifacts from one committed source.
pac solution version --patchversion $BUILD_NUMBER
pac solution pack --zipfile ./out/ContosoSales.zip \
  --folder ./src/ContosoSales --packagetype Unmanaged
pac solution pack --zipfile ./out/ContosoSales_managed.zip \
  --folder ./src/ContosoSales --packagetype Managed

# Gate the build on static analysis before anything reaches an environment.
pac solution check --path ./out/ContosoSales_managed.zip \
  --outputDirectory ./checkresults

# Release stage. Managed only, into test first, then production on approval.
pac auth select --name test
pac solution import --path ./out/ContosoSales_managed.zip \
  --settings-file ./config/test.deploymentsettings.json \
  --activate-plugins --publish-changes --force-overwrite --async

Deployment settings: the values that must differ per environment

The step that separates a pipeline which works from a pipeline which is safe. Without it, a solution exported from development carries development endpoints and development identities into production, and does so silently.

Environment variables and connection references
# Generate the template once from the packed managed artifact.
pac solution create-settings --solution-zip ./out/ContosoSales_managed.zip \
  --settings-file ./config/deploymentsettings.json

# It lists every environment variable and every connection reference in the
# solution, which is exactly the set of values that must differ per environment.
# Commit one filled copy per environment, with secrets left as placeholders
# that the pipeline substitutes from its own secret store at release time.

pac auth select --name prod
pac solution import --path ./out/ContosoSales_managed.zip \
  --settings-file ./config/prod.deploymentsettings.json --async

What does manual export and import actually cost you?

Not an argument from best practice. These are the specific, recurring failures that bring a system to us, roughly in the order of how much damage each one does by the time somebody calls.

Nobody can tell you what changed, or who changed it

The single most expensive property of a hand carried ZIP is that it has no history. A binary in a shared folder called ContosoSales_v4_final_FIXED.zip records neither what is inside it nor what it differs from. An unpacked solution in Git records both, at the level of a single form field, a single option set value, or a single line of a plugin. Almost every question that gets asked in an incident, which is what changed and when and why, becomes a git log rather than an investigation.

Two developers overwrite each other and neither of them notices

Export is a whole solution operation. If one developer exports at ten in the morning and another exports the same solution at two in the afternoon, the second ZIP silently contains a snapshot of everything, including a half finished change the first developer had not intended to promote. Nothing warns anybody. Unpacked source with branches and pull requests makes this a merge conflict, which is a conversation, rather than a deployment, which is an incident.

A production import goes in unmanaged and can never be taken out

This is the mistake with the longest tail. An unmanaged component imported into production becomes part of the base layer of that environment permanently. There is no uninstall. The only routes back are a restore from backup or reversing the change by hand, component by component, and the second one is what most teams end up doing. Managed imports, by contrast, sit in a removable layer, and a bad release can be rolled back by reinstalling the previous managed artifact.

The environment specific values are baked into the file

A solution exported from development carries development values: the SharePoint site the flow points at, the endpoint a custom connector calls, the account an integration authenticates as, the record identifiers a plugin was configured with. Imported into production without intervention, the production system quietly reads and writes development data. The fix is environment variables and connection references, resolved at import time from a deployment settings file, and the CLI generates that file for you with pac solution create-settings.

The version number stops meaning anything

Manual promotion almost always drifts on versioning, because incrementing the version is a step somebody has to remember and nothing enforces. Then two environments both claim to hold 1.0.0.4 and they do not contain the same thing. Worse, an import of a solution whose version was not incremented can leave cached artifacts in place while the import log reports success, which produces the specific and maddening symptom of a change that deployed cleanly and did not appear.

The person who knows the import order leaves

Real systems are several solutions with dependencies between them, plus configuration data, plus a set of manual post deployment steps that live in somebody head or in a document last updated a year ago. That knowledge is a staffing risk rather than a technical one right up until the week it becomes both. A pipeline definition in the repository is the same knowledge written in a form that executes, which means it cannot quietly go stale without the build failing.

Testing happens in production because there is nowhere else

When promotion is expensive and manual, teams naturally do less of it, and the honest end state of that is changes made directly in the live environment because getting them there properly takes an afternoon. Every environment we are asked to look at where customizations were made in production got there by this route. It is not a discipline problem. It is what happens when the correct path costs an hour and the incorrect one costs five minutes.

Rollback is a conversation about backups rather than a command

Ask a team using manual promotion what happens if tonight release is wrong, and the answer is usually a restore, which means losing the day of business data as well as the bad release. With managed solutions built from tagged source, the answer is that the previous artifact is still in the build store and reinstalling it is a pipeline run. The difference between those two answers is the entire reason the finance director eventually agrees to pay for this work.

The layered, unreviewable configuration these habits produce is the same condition we describe from the other end on our Dynamics 365 customization mistakes page, and when it has gone far enough that nobody can safely change anything, it becomes a project rescue and takeover.

Why is manual deployment an audit finding, and what is the auditor actually asking for?

If weekend deployments, direct production edits and missing environments have been written up as a compliance finding, the auditor is not asking you to buy tooling. In audit language they are asking for four things: segregation of environments, evidence that each change was reviewed and approved before it reached production, a traceable link from a requirement to the artefact deployed, and a demonstrated rollback capability. Each maps to a concrete Power Platform control: solution-based deployment, source control, a service principal pipeline and a gated approval. The minimum set below satisfies the finding without stopping the business, and most of it is the method this page already describes.

Read the exact wording of your own finding first. Frameworks and auditors phrase change management requirements differently, and the controls below are the platform side of the answer rather than a statement of what any particular standard requires.

The four asks, mapped to controls and evidence

Segregation of environments

What it means: Changes are built and tested somewhere other than production, and the people who build them cannot also change production directly.

The control: Separate development, test and production environments; unmanaged work only in development; managed solutions only downstream; maker and customizer rights removed in production, with deployment done by a service principal rather than a person.

The evidence you hand over: The environment list with each purpose, the security role assignments in production, the identity the release pipeline runs as, and a periodic check showing no new unmanaged layers in production.

Evidence of review and approval before production

What it means: Someone other than the author looked at the change, and someone with authority approved it reaching production, before it did.

The control: pac solution sync onto a branch so the change is a readable diff, a pull request reviewed by someone other than the author, and a gated approval on the production stage of the pipeline.

The evidence you hand over: The pull request with its reviewer and date, and the pipeline approval record with the approver and timestamp, both kept by Azure DevOps, GitHub or Power Platform Pipelines rather than in an email thread.

A traceable link from requirement to deployed artefact

What it means: For any component in production, you can show which approved requirement caused it and which build put it there.

The control: A work item reference on every branch or pull request, the version stamped by the build with pac solution version, artefacts packed only from committed source, and a release that records which artefact went where.

The evidence you hand over: A chain you can walk in either direction: work item, pull request, commit, build number, release run, and the solution version shown in production by pac solution list or the solution history.

A demonstrated rollback capability

What it means: Not a statement that rollback is possible, but proof that the team has done it and knows what it does and does not undo.

The control: Managed solutions built from tagged source, previous artefacts retained by the pipeline, a pre-release environment backup where your environment type supports it, and a rollback or roll-forward rehearsed in test.

The evidence you hand over: A dated record of the rehearsal: what was deployed, what was reverted, how long it took, and what it did not reverse, such as data written by the release. Reinstalling an earlier managed version can mean removing the newer one first, which has consequences for columns it added, so the rehearsal is where that is found out.

The minimum set that closes the finding without stopping the business

In order. The first item removes the exposure and costs nothing; the rest build the evidence. Full test automation, solution checker tuning and standardisation across many environments are worth doing, but they are not what the finding asks for and they should not delay closing it.

  1. Remove direct change rights in production this week

    Take maker, system customizer and equivalent rights away from everyone who does not need them in production, and keep one named break-glass administrator whose use is logged and reviewed. This costs nothing, is the fastest visible response to the finding, and stops the finding growing while everything else is built.

  2. Stand up the missing environments and move to solutions

    Development, test and production at minimum, with all work in named solutions under one publisher, unmanaged only in development and managed downstream. Where production already holds direct changes, capture them into development first, as step 11 of the migration above describes, so the first managed release does not overwrite something the business depends on.

  3. Put the solution in source control with a reviewed pull request

    pac solution sync onto a branch and a pull request approved by someone other than the author. This delivers the review evidence and most of the traceability on its own, and it can run for a few weeks before any pipeline exists.

  4. Deploy through one pipeline as a service principal, with a gate before production

    The simplest pipeline that removes people from the deployment path and records an approval: Power Platform Pipelines where its prerequisites fit your licensing, or Azure DevOps or GitHub where you already run them. Weekend deployments can continue if the business needs them; what changes is that they run from an approved artefact rather than from somebody laptop.

  5. Reference a work item on every change

    One field or naming convention that links each pull request to the requirement or ticket behind it. Without this the rest of the chain exists but cannot be walked from the requirement end, which is usually where the auditor starts.

  6. Rehearse a rollback once and write it down

    Deploy a release to test, roll it back or roll forward with a fix through the same pipeline, and record what happened, including what it did not undo. One dated rehearsal is stronger evidence than a procedure document nobody has run.

  7. Define the emergency change route

    Urgent fixes still go through the pipeline, with a shortened review and a retrospective approval recorded within an agreed period. A control with no fast path gets bypassed the first time something breaks on a Friday, and a bypass is a new finding.

  8. Check production for unmanaged change on a schedule

    A monthly look at the solution layers and the default solution in production, with the result kept. It is the evidence that segregation is operating rather than only configured.

A management response usually needs an honest description of the current state as well as the plan. Our Dynamics 365 health check and technical audit is an independent, fixed-scope read of solution layering, environment topology and how changes actually reach production, which gives you evidence for that description and an ordered remediation plan. Putting the controls in place is the ALM work described on the rest of this page, and we deliver it directly or on a white-label basis for Microsoft partners.

How do you migrate to the PAC CLI and a pipeline, step by step?

Steps 1 to 3 are decisions that are cheap now and expensive in a year. Steps 4 to 7 put the source in a repository and make the diff the review, which is where most of the benefit lives. Steps 8 to 10 automate the promotion. Steps 11 and 12 are the parts teams skip and then regret, which are reconciling what is already in production and naming an owner. The order matters more than the tooling does.

  1. Inventory what the environments actually contain

    Connect to production with pac auth create and run pac solution list, then do the same against test and development. You are looking for three things: how many solutions there are and whether they are managed or unmanaged, whether the same solution carries the same version in each environment, and how much sits in the default solution, which is where everything made directly in an environment ends up. Write the answer down before you decide anything, because the plan for an environment with three managed solutions and a clean default is completely different from the plan for one with eleven unmanaged solutions and two years of direct edits.

  2. Settle the publisher and the prefix, once, before anything else

    Every component carries the prefix of the publisher that created it, and changing a publisher later does not rename anything: it creates new components alongside the old ones. So this is the cheapest decision on the page today and one of the most expensive in a year. Pick one publisher for your organisation, give it a short prefix, use it in every environment, and make sure the maker portal default publisher is set to it so that anything created in a hurry still lands in the right place.

  3. Decide the solution boundaries deliberately

    One enormous solution containing everything makes every deployment a deployment of everything, and imports get slower until nobody wants to run one. A separate solution per developer makes dependency order a permanent puzzle. The shape that works for most mid sized systems is a small number of solutions along real boundaries: the core data model, one per functional area that ships independently, and one for code components such as plugins and PCF controls. Dependencies must run one way only, and it is worth drawing them on paper because a cycle is much easier to see before it exists.

  4. Install the CLI and create one authentication profile per environment

    Install the Power Platform CLI, then run pac auth create against development, test and production and give each profile a name. Get into the habit of running pac auth list and pac org who before any command that writes. This sounds trivial and it is the single most common cause of the worst kind of accident in this stack, which is running an import against production while believing you are pointed at a sandbox.

  5. Take the baseline into source control with clone, or with export and unpack

    Run pac solution clone against the development environment, which exports the solution and unpacks it into a folder of individual component files in one step, and also writes the project files needed to build it later. If you would rather see both halves, run pac solution export with the managed flag set to false, then pac solution unpack on the resulting ZIP, adding the option that also unpacks canvas app sources so they become diffable. Commit the unpacked folder. Do not commit the ZIP, and add the artifact folder to gitignore, because the ZIP is a build output from this point on.

  6. Make the diff the review, using sync on a branch

    From now on, work happens in the development environment as it always did, and pac solution sync brings it down onto a branch as a readable change set. The pull request diff becomes the review, which is where this method starts paying for itself: a column nobody asked for, a business rule left switched on after a demo, a form change made during a workshop and forgotten, all of them become visible before they travel. Agree with the team that the branch is the unit of work and that nothing reaches test without going through one.

  7. Remove environment specific values with environment variables and connection references

    Every value that differs between environments has to come out of the components and into an environment variable or a connection reference: endpoint URLs, site addresses, feature switches, the identity an integration authenticates as. Then run pac solution create-settings against the packed artifact to generate a deployment settings file listing all of them, and keep one filled copy per environment in the repository, with secrets left as placeholders that the pipeline substitutes at release time from its own secret store. Skipping this step is the reason most first pipelines deploy successfully and then read development data in production.

  8. Build the artifacts on a build agent, never on a workstation

    The build stage checks out the committed source, runs pac solution version to stamp the build number, then runs pac solution pack twice to produce the unmanaged and managed artifacts from that same source. Both are published as build artifacts and kept. This is the step that makes the whole thing auditable, because from here on every artifact that exists is traceable to a commit, and the question of what is running in production has an answer that does not require anybody to remember anything.

  9. Gate the build on the solution checker

    Run pac solution check against the packed managed artifact and have the pipeline fail the build above an agreed severity. The value is not that the checker is always right, because it is not, and every team accumulates a small list of rules it deliberately suppresses with a reason written next to each one. The value is that the list is explicit and reviewed rather than implicit and forgotten, and that a genuinely dangerous pattern gets caught before somebody has to read it in an incident report.

  10. Release managed only, into test first, with an approval before production

    The release stage authenticates as a service principal, then runs pac solution import against test with the settings file for test, publishing changes and activating plugins, and running asynchronously so a large solution does not time out. Production is the same command with the production settings file behind a manual approval. Use pac solution upgrade rather than a plain import for releases that delete components, because an import updates and adds but does not remove, and that difference is how orphaned columns, views and processes accumulate in a long lived environment.

  11. Deal with the unmanaged layers that are already in production

    Almost every environment moving to this method has direct changes in production that exist nowhere else, and pretending otherwise is how a first pipeline run overwrites something the business depends on. Find them, decide one at a time whether each is wanted, rebuild the wanted ones properly in development so they arrive through the pipeline, and remove the rest. This is genuinely the slowest part of the work and it is not optional, because until it is done the repository is not actually the source of truth and everyone is relying on a promise that it is.

  12. Put a name against the pipeline and rehearse the rollback

    A pipeline nobody owns rots in about a quarter: a secret expires, a service principal loses a role, an agent image changes, and the team quietly goes back to carrying ZIP files while telling itself this is temporary. Name an owner, put the credential expiry dates in a calendar, and rehearse a rollback once by reinstalling the previous managed artifact into test on purpose. Doing that once, in daylight, is worth considerably more than a document describing how it would work.

If step 1 turns up more than you expected, and it usually does, our Dynamics 365 health check is the fixed scope version of that inventory, covering solution layering, plugins, scripts and environment topology in one pass.

Which pipeline should you use, and do you need one yet?

Pipelines is assumed to mean Azure DevOps, and for a lot of teams it should not. The choice matters much less than whether the solution source is in a repository at all, and it is reversible, so it deserves an afternoon rather than a month.

Power Platform pipelines, configured in the platform itself

Microsoft ships a pipelines capability inside the platform that promotes solutions between environments with approvals, without anybody writing YAML or maintaining an agent. For a team whose changes are configuration rather than code, and whose bottleneck is that promotion is manual rather than that promotion is untested, this is very often the right first move and it can be running in a day. Its limits are real: less room for custom build steps, less control over what gates a release, and a weaker story when you need the same commit to build a plugin assembly and run a test suite on the way through.

Azure DevOps pipelines with the Power Platform build tools

The most common choice on larger systems, and the one to pick when the work already lives in Azure DevOps or when solutions ship alongside C# plugins, PCF controls or integration code. There are supplied tasks that wrap the same operations described on this page, and you can also call pac directly, which we tend to do for anything the tasks do not cover so that the pipeline reads the same as the commands a developer runs locally. Service connections handle authentication, environments handle approvals, and the artifact retention gives you the previous release to roll back to.

GitHub Actions

Functionally equivalent for this purpose, and the natural choice when the source already sits in GitHub. There are published actions covering export, pack, import and the solution checker, authentication uses a service principal held in repository or environment secrets, and environment protection rules give you the approval gate before production. The tradeoff against Azure DevOps is mostly about which tool your organisation already administers rather than about capability.

Source control with no pipeline at all, as a deliberate first stage

Worth naming explicitly because it is the step most teams should take first and skip. Getting the solution unpacked into Git and running sync on a branch delivers a large share of the total benefit, which is history, review and the ability to answer what changed, and it can be done in an afternoon without any infrastructure. The promotion can stay manual for a few weeks while people get used to the diff being the review. Automating promotion into a repository nobody yet trusts just makes the wrong thing happen faster.

What goes wrong on the first pipeline?

None of these are in the command reference, because none of them are properties of a command. They are the things that make a correctly written pipeline produce a wrong outcome, and every one of them is something we have been called in to unpick.

Committing the ZIP as well as the unpacked folder

It seems harmless and it quietly defeats the purpose. Two representations of the same thing will drift, reviewers start reading whichever one is easier, and eventually somebody builds from the committed ZIP rather than from source. Put the artifact directory in gitignore on day one and keep exactly one source of truth.

Importing unmanaged into test or production because it was quicker

Usually done once, under deadline pressure, with the intention of tidying it afterwards. It cannot be tidied. An unmanaged component in a target environment becomes part of its base layer permanently and the managed solution can no longer control it. This one exception is how a large share of the environments we are asked to rescue reached the state they are in.

Forgetting that a plain import never deletes anything

Remove a column in development, promote, and the column is still in production. Import updates and adds. Deletions travel only through a staged upgrade, which is what pac solution upgrade is for. Teams that do not know this accumulate orphaned components for years and then discover them during a data migration, which is the worst possible moment.

Not incrementing the version, then debugging the wrong thing

An import of an unchanged version number can complete successfully while the platform continues serving what it already had. The symptom is a clean import log and no visible change, and people lose entire days to it. Stamping the version in the build stage rather than trusting anybody to remember removes the failure mode altogether.

Authenticating the pipeline as a person

It works on the first run, which is why it happens. Then the person changes their password, or leaves, or their account gets a conditional access policy, and the release pipeline fails at the worst moment with an error about authentication that takes an hour to interpret. Pipelines authenticate as a service principal with an application id, a client secret or certificate, and the minimum roles it needs, and the expiry date of that secret goes in a calendar.

Canvas apps and flows treated as if they diff like configuration

Unpacked canvas app sources are readable but noisy, and a trivial edit can produce a large diff, so reviewers need to know what to skim and what to actually read. Flows carry connection references that must be mapped per environment or the first import into test fails with an error that names an identifier rather than a flow. Both are manageable and both surprise teams on their first pipeline run.

Configuration data left out of the pipeline

Reference records, meaning the rows the solution logically depends on rather than transactional data, are not part of a solution and do not travel with it. If they move by spreadsheet, then the pipeline is only automating half the release and the half it is not automating is the half that breaks silently. pac data export and pac data import with a schema file put that half in the pipeline where it belongs.

Starting with the pipeline instead of with the repository

The pipeline is the visible part, so it is where the enthusiasm goes. But a pipeline promoting a repository that does not yet match production is an automated way of deploying a fiction. Get the baseline right, reconcile the unmanaged layers, let the team live with the diff as a review for a couple of weeks, and then automate. The order matters more than the tooling choice does.

Code components have their own version of the same discipline, since a PCF control ships inside a solution and carries a manifest version of its own. The packaging and promotion path for those is set out on our Power Platform developer page, alongside the release checklist we run on every control.

How do you standardise ALM across many environments and clients?

Standardising across many environments and clients

The failure is familiar to anyone looking after more than a handful of environments for more than one client. A solution is exported by hand from whichever environment looked most current and imported into the next one, where it works. The same ZIP goes into another client environment and fails on a missing dependency, or it imports cleanly and a flow then writes to the wrong site because a value was typed into it directly. Each environment has been fixed in place a little differently, configuration has drifted, and nobody can say any longer which environment is the reference. More tooling does not fix this on its own. The order of operations does.

The steps below extend the single estate migration above to a team running the same product for several clients. An admin can drive the inventory, the governance and much of the configuration, while changing what is inside components, splitting solutions and writing the pipeline need a developer, as the table further down sets out. If the differences you are standardising are between regions of one group rather than between clients, the same core and extension pattern is set out as an architecture decision on our multi region Dynamics 365 standardisation guide.

  1. Inventory every environment and every solution

    Run pac admin list to enumerate the environments in the tenant, which needs an admin role, then authenticate against each one and run pac solution list, recording for every solution its unique name, version, publisher and whether it is managed or unmanaged. Where a component behaves differently in two environments, open the solution layers view for that component in the maker portal to see which solution, or which unmanaged layer, is actually winning. Keep the result as one file in the repository with a row per environment and client. This is usually the step where a team discovers that two clients run different versions of what everyone assumed was the same core.

  2. Separate configuration data from schema

    A solution carries schema and metadata: tables, columns, forms, views, flows and plugins. It does not carry rows. Split the rows into two kinds. Reference data that should be identical everywhere, such as category lists or a shared product structure, moves with the Configuration Migration tool or pac data export and pac data import, driven by a schema file kept in source control. Environment configuration that differs per environment or per client, such as endpoints, mailbox addresses or feature switches, should not be rows copied between environments at all, and belongs in the next step.

  3. Adopt environment variables and connection references everywhere

    Every value that differs between environments or clients comes out of the components and into an environment variable, and every flow uses connection references rather than connections owned by a person. Generate a deployment settings file per solution with pac solution create-settings and keep one filled copy per client per environment, with secrets as placeholders the pipeline substitutes. After this step the same managed artefact can be installed in any environment, and the differences between clients live in small files you can diff and review.

  4. Move to managed solutions downstream, with unmanaged only in development

    Test, acceptance and production for every client receive managed solutions only. Unmanaged work lives in development: one development environment for the shared core, and one development environment per client in which the core is installed as managed and only that client extension is unmanaged. That arrangement means a client specific change cannot edit a core component by accident, because the core is not editable where the client work happens. Where production already holds unmanaged changes, reconcile them before the first managed import, as described in step 11 of the migration above.

  5. Define one per client release template

    Give every client the same shape: the shared core solution under your own publisher, a client extension solution with its own publisher and prefix that depends on a minimum core version, a deployment settings file for each of that client environments, and a reference data schema file. The release order is always the same: core managed, then the client extension managed, then reference data, then a short smoke check. A client that needs something the core cannot do gets it in its extension or as a configurable option added to the core, never as an edit to a core component.

  6. Automate with the Power Platform CLI and one parameterised pipeline

    Build the core once from committed source and release that same artefact to every client, then build each client extension and release it with that client settings files. Use Power Platform Build Tools for Azure DevOps, GitHub Actions for Power Platform, or Power Platform Pipelines where the simpler in product route fits. Write one pipeline definition that takes the client and environment as parameters rather than a copied pipeline per client, because copies drift in exactly the way the environments used to.

  7. Govern the estate so it stays standard

    Decide who may create environments, which data loss prevention policies apply, and who has authority to decide whether a request belongs in the core or in a client extension. Re-run the inventory from step 1 on a schedule and compare it with the previous run, so drift shows up as a difference in a file rather than as a failed release. The ownership and governance model that keeps this going after the initial project is the subject of our Center of Excellence guide.

The per client release template

A shared core solution, a client extension solution with its own publisher and prefix, a deployment settings file per environment, and one release order that never changes. An extension adds to the core and never edits a core component, so a core release can be installed under every client without anybody checking what each one has changed.

Repository layout and release order
# One repository, one shape for every client.
src/
  Core/                          # shared core solution, your publisher and prefix
  Clients/
    ClientA.Extension/           # own publisher and prefix, depends on Core
    ClientB.Extension/
config/
  core/
    clienta-test.deploymentsettings.json
    clienta-prod.deploymentsettings.json
    clientb-test.deploymentsettings.json
    clientb-prod.deploymentsettings.json
  clients/
    clienta-test.deploymentsettings.json
    clienta-prod.deploymentsettings.json
data/
  reference-data.schema.xml      # Configuration Migration schema, identical rows
pipelines/
  release.yml                    # parameters: client, environment

# Release order, the same for every client and every environment:
#   1. Core managed        (pac solution import with the core settings file)
#   2. Client extension    (pac solution import with the client settings file)
#   3. Reference data      (pac data import with the committed schema)
#   4. Smoke check, then approval before production

What is free, and what needs licensing

The tooling itself costs nothing. The platforms it runs on, and some of the governance features, do. Microsoft revises Power Platform licensing regularly, so treat this as the shape of the answer and confirm the current position before deciding on cost grounds.

ItemFree or licensedWhat to know
Power Platform CLI (pac)Free tool from MicrosoftRuns against environments you already license. No separate charge for the CLI itself.
Power Platform Build Tools for Azure DevOpsFree extensionAzure DevOps has its own plans for users and parallel jobs, which apply as for any other pipeline.
GitHub Actions for Power PlatformFree actionsYour GitHub plan and its Actions usage allowance apply.
Configuration Migration tool and pac dataFree toolsMove reference data between environments you already license.
Power Platform PipelinesIn product feature, with prerequisitesMicrosoft documentation requires the target environments to be Managed Environments. Check current Microsoft guidance, because the prerequisites have changed before.
Managed EnvironmentsNeeds qualifying licencesGovernance features such as enforcing the solution checker on import sit here. Users of a Managed Environment need qualifying premium licences; confirm against current Microsoft licensing before planning around it.
Additional environmentsUses tenant capacityEach environment with Dataverse draws on the tenant storage capacity entitlement, so a per client development environment is a capacity decision as well as an ALM one.

What an admin can do, and what needs a developer

Most of the inventory and governance is admin work. Anything that changes what is inside a component, or that writes a pipeline, is developer work.

TaskWho
Inventory environments and solutions with pac admin list, pac solution list and the solution layers viewAdmin
Set data loss prevention policies, environment creation rules and Managed EnvironmentsAdmin
Configure Power Platform Pipelines between existing environmentsAdmin
Fill in and maintain the deployment settings file for each client and environmentAdmin, with a developer reviewing the first set
Run reference data moves with the Configuration Migration tool once the schema file existsAdmin
Replace hard coded values in flows, plugins and scripts with environment variables and connection referencesDeveloper
Split an existing solution into a shared core and client extensions, and reconcile publishers and prefixesDeveloper
Write and maintain Azure DevOps or GitHub pipelines with service principal authenticationDeveloper
Remove unmanaged layers already sitting in client production environmentsDeveloper led, with admin sign off

Keeping an estate like this standard after the first project is a governance and capability question as much as a tooling one. How to set up the environment strategy, data loss prevention policies and ownership model, and how to build that capability with a nearshore team, is covered in our Power Apps Center of Excellence guide.

How does Solzet set this up?

Release engineering is part of how we deliver rather than something we sell separately, so this is what it looks like when we do it, on a new build and on a system that already exists.

We do this as part of delivery rather than as a separate product

Every environment we build gets separate development, test and production, unpacked solution source in your repository, and a build and release pipeline, and it is settled in the first week rather than added when the first release goes wrong. It is the least interesting decision on a project and the one whose absence does the most damage, which is why we do not offer a version of the engagement without it.

We retrofit it onto systems that already exist

The harder and more common job. An environment with years of direct changes, several unmanaged solutions, no repository and a promotion process that is one person and a shared folder. The work is inventory, publisher and prefix reconciliation, solution boundaries, the baseline into Git, the deployment settings, the pipeline, and then the slow part, which is deciding one at a time what to do with each unmanaged change already sitting in production.

It runs in your tooling, not ours

Azure DevOps, GitHub, or the in product pipelines, in your tenant, under your accounts, with the repository in your organisation. We work inside what you already administer rather than asking you to adopt something new, and the pipeline definitions are yours in the statement of work along with the solution source, so nothing about this arrangement is difficult to end.

Then somebody owns it after we leave

A pipeline with no owner degrades within a quarter. We hand over with the definitions in your repository, the credential expiry dates written down, a rehearsed rollback, and either a named person on your side or a monthly support allocation with us that covers the release path along with the two Microsoft release waves a year. Which of those it is should be decided before go live rather than found out afterwards.

The engagement models, certifications and what the wider role covers are on our Power Platform developer and Dynamics 365 developer pages. If the release path is one symptom of a project that has already stalled, the rescue and takeover service page describes how we stabilise an environment first and put it on a controlled release path second.

What do teams ask about Power Platform ALM with the PAC CLI?

Why is manual Dynamics 365 or Power Platform deployment written up as a compliance audit finding?

Because it fails the change management controls most audit frameworks test, not because the tooling is old. When changes are exported and imported by hand, made directly in production, or deployed at weekends with no test environment, the organization cannot show four things an auditor looks for: that environments are segregated, that each change was reviewed and approved before production, that a production component can be traced back to an approved requirement, and that a bad release can be reversed. The fix is to put those controls in place and keep the evidence they produce, which is what solution-based deployment, source control, a service principal pipeline and a gated approval do. Read the exact wording of your own finding, since frameworks and auditors phrase the requirement differently.

What does an auditor mean by segregation of environments for Power Platform?

That changes are built and tested somewhere other than production, and that the people who build them cannot also change production directly. In Power Platform that means separate development, test and production environments, unmanaged work only in development, managed solutions only in test and production, maker and customizer rights removed in production, and deployments run by a service principal rather than a named person. The evidence is the environment list with purposes, the production role assignments, the pipeline identity, and a periodic check showing no new unmanaged layers in production.

What evidence of review and approval do auditors expect before a Power Platform change reaches production?

A record, made at the time, that someone other than the author reviewed the change and someone with authority approved its release. The practical form is a pull request on the diff produced by pac solution sync, approved by a second person, and an approval gate on the production stage of the pipeline in Azure DevOps, GitHub or Power Platform Pipelines. Both tools keep the approver and timestamp. An email saying a change was approved is weak evidence because it is not tied to the artefact that was deployed.

How do we show traceability from a requirement to a deployed Dynamics 365 solution?

Build a chain that can be walked in both directions. Every branch or pull request references the work item behind it, the build stamps a version with pac solution version and packs the artefact only from committed source, and the release records which artefact went to which environment. An auditor can then start from a requirement and find the production version that delivered it, or start from a component in production, read the solution version with pac solution list or the solution history, and walk back through the release, build, commit and pull request to the requirement.

How do we demonstrate rollback capability for Power Platform deployments?

By doing it once in test and keeping the record. Build managed solutions from tagged source, keep previous artefacts in the pipeline, take an environment backup before production releases where your environment type supports it, and rehearse either a rollback or a roll-forward fix through the same pipeline. Record what was reverted, how long it took, and what it did not reverse, such as data written by the release. Reinstalling an earlier managed version can require removing the newer one first, with consequences for columns it added, which is exactly why auditors ask for a demonstration rather than a statement. Check current Microsoft documentation for backup and restore options for your environment type.

What is the minimum set of change controls that closes a manual deployment audit finding without stopping the business?

Eight controls, in this order. Remove direct change rights in production and keep one logged break-glass administrator. Stand up development, test and production and move to solutions, managed downstream. Put the solution in source control with a pull request reviewed by someone other than the author. Deploy through one pipeline running as a service principal with an approval gate before production. Reference a work item on every change. Rehearse a rollback once and record it. Define an emergency change route that still uses the pipeline with retrospective approval. And check production for unmanaged change monthly, keeping the result. Full test automation and multi-environment standardisation can follow later.

How do we handle urgent production fixes without breaking change control?

Give them a defined fast path rather than an exception. An urgent fix is still made in development, still packed by the pipeline and still deployed by the service principal, but with a shortened review and an approval recorded retrospectively within an agreed period, and a note of why it was urgent. The break-glass administrator account exists for the rare case where even that is too slow, and every use of it is logged and reviewed. Without a fast path, the first Friday outage produces a direct production edit, and that edit becomes the next audit finding.

What are the main Power Platform CLI pac solution commands for export, import, pack and unpack?

Six commands cover almost everything. pac solution export downloads a solution from the connected environment as a ZIP, taking the solution unique name, an output path, and whether it is managed or unmanaged. pac solution unpack explodes that ZIP into a folder of individual component files, with an option that also unpacks canvas app sources so they are diffable. pac solution pack is the inverse, rebuilding a ZIP from an unpacked folder as managed or unmanaged. pac solution import pushes a ZIP into the connected environment, with options to publish customizations, activate plugins, run asynchronously and apply a deployment settings file. pac solution clone does export and unpack in one step and also writes the project files, which is what you use for the first baseline. pac solution sync refreshes an already cloned folder from the environment, which is what you use every day afterwards. Around those sit pac auth create for authentication, pac solution version for the build number, pac solution check for static analysis and pac solution create-settings for the per environment values. Flags change between CLI releases, so confirm yours with pac help solution.

What is the difference between pac solution export and pac solution unpack?

They are consecutive halves of the same operation and people conflate them constantly. Export is a server operation: it asks the environment for a solution and gives you a ZIP file, which is a single binary that no diff tool can read and no reviewer can review. Unpack is a purely local operation on that file: it takes the ZIP and writes out a folder tree with one file per component, so a form is a file, an option set is a file, a plugin step is a file. Export gets the change out of the platform, unpack makes it reviewable and mergeable. Committing the output of export rather than the output of unpack is the most common way teams end up with version control that gives them almost none of the benefit of version control.

Should I use pac solution clone or pac solution export followed by unpack?

Clone once, then sync from then on, and reach for the explicit pair when you need control. Clone runs export and unpack together and additionally creates the project files that let the folder be built as a repeatable artifact, which makes it the right command for taking the first baseline of a solution that has never been in source control. After that, sync refreshes the cloned folder from the environment and gives you the change set as a diff in your working tree, which is the daily loop. The explicit export and unpack pair is still worth knowing and still worth running once, because it is what the pipeline and the tooling are doing underneath, and because there are situations, such as exporting with specific included settings or against a target version, where you want each half separately.

Can I keep exporting and importing solutions by hand if the team is small?

You can, and for a single developer on a single small solution it is a defensible choice for a while. The point at which it stops being defensible is not a headcount, it is the arrival of any one of four things: a second person making changes, a production environment that real money depends on, an auditor or a customer asking what changed and when, or the first release that has to be rolled back. Each of those turns a hand carried ZIP from an inconvenience into a risk. The cheap middle position is worth naming: get the solution unpacked into Git and use sync on a branch, which takes an afternoon and delivers history and review, and leave promotion manual for a while longer. That gets you most of the benefit before you have written a line of pipeline.

What is the difference between managed and unmanaged solutions, and why does it matter here?

Unmanaged is the editable state, which is why development environments hold unmanaged solutions and why that is the only place components should be edited. Managed is the sealed state: the components are locked to the solution that installed them, and crucially the whole thing can be uninstalled, which is what makes a release reversible. The rule that follows is simple and should never be broken, not even once under deadline pressure. Development is unmanaged, test and production only ever receive managed. The reason it should never be broken is that an unmanaged component imported into a target becomes part of the base layer of that environment permanently, with no uninstall available, and the only routes back are a restore from backup or undoing it by hand. That single exception, made once, is how a large share of the environments we are asked to take over reached the state they are in.

How do I stop development values reaching production when the pipeline deploys?

Take them out of the components. Every value that differs between environments becomes an environment variable, and every connector a flow uses becomes a connection reference, so that no component contains an endpoint, a site address or an identity directly. Then run pac solution create-settings against the packed artifact to generate a deployment settings file listing all of them, keep one filled copy per environment in the repository, and pass the right one to pac solution import at release time. Secrets stay as placeholders in the committed file and are substituted by the pipeline from its own secret store. Teams that skip this get a first pipeline that deploys perfectly and then quietly reads and writes development data from production, which typically goes unnoticed for a fortnight.

Why did my solution import succeed but the change is not there?

The first thing to check is the version number, because an import of a solution whose version was not incremented can complete successfully while the platform continues serving what it already had. Stamping the version in the build stage with pac solution version removes that failure mode entirely. The second thing is publishing, since some changes are inert until customizations are published, which is what the publish changes option on import is for. The third is that a plain import updates and adds but never deletes, so if what you expected was a removal rather than a change, it will not have happened and you need a staged upgrade instead. And the fourth, which is less common but wastes the most time, is a dependency that exists in the development environment but not in the target, so a component imported without the thing it depends on and behaves as though it is not there.

Do I need Azure DevOps, or can I use the pipelines built into the platform?

For many mid sized teams the in product pipelines feature is the right answer and it is available in a day, because it promotes solutions between environments with approvals and needs no YAML, no agent and no separate tool to administer. Choose Azure DevOps or GitHub Actions when the release has to do more than move a solution: build a plugin assembly or a PCF control from the same commit, run tests, run the solution checker as a gate, deploy configuration data, or coordinate with something outside this platform. Also choose them when your organisation already runs one of the two, because the pipeline that gets maintained is the one sitting in the tool people already open. It is a reversible decision either way, and it is far less important than whether the solution source is in a repository at all.

We already have years of changes made directly in production. Where do we start?

Not with the pipeline. Start with pac solution list against every environment and an honest inventory of what each one holds, including how much sits in the default solution, which is where anything made directly in an environment ends up. Then reconcile the publisher and prefix, decide the solution boundaries, and take a baseline of development into Git. The slow part, and it is genuinely slow, is going through the changes that exist only in production and deciding one at a time whether each is wanted, rebuilding the wanted ones properly in development so they arrive through the pipeline, and removing the rest. Until that is done the repository is not the source of truth and a pipeline is only automating a fiction. This is the shape of work we do as a project rescue, and it is described on our project rescue and takeover page.

How do you standardise Power Platform ALM across many environments and many clients?

In a fixed order. First inventory every environment and solution, using pac admin list for the environments and pac solution list plus the solution layers view for what each one holds. Second, separate configuration data from schema, moving reference data with the Configuration Migration tool or pac data and a committed schema file. Third, move every value that differs per environment or client into environment variables and connection references, with a deployment settings file per client per environment. Fourth, make every downstream environment managed only, with unmanaged work confined to development. Fifth, give every client the same release template: a shared core solution plus a client extension solution with its own publisher. Only then automate it with the Power Platform CLI and one parameterised pipeline in Azure DevOps, GitHub Actions or Power Platform Pipelines.

Is Power Platform ALM free, or does it need extra licensing?

The core tools are free: the Power Platform CLI, Power Platform Build Tools for Azure DevOps, GitHub Actions for Power Platform and the Configuration Migration tool carry no charge of their own. Azure DevOps and GitHub have their own plans for users and pipeline usage. Power Platform Pipelines, the in product option, requires the target environments to be Managed Environments according to Microsoft documentation, and Managed Environments need qualifying premium licences for their users. Additional environments also draw on tenant Dataverse capacity. Microsoft revises licensing regularly, so confirm the current position with Microsoft or your licensing partner before choosing a route on cost grounds.

Should each client get its own copy of the solution, or a shared core with client extensions?

A shared core with client extensions, almost always. Copies of one solution per client diverge within months, and every fix then has to be applied several times by hand. A shared core solution, released as the same managed artefact to every client, plus a thin extension solution per client with its own publisher and prefix, keeps the common product in one place and the differences visible. Values that differ between clients belong in environment variables and deployment settings files rather than in the extension. The exception is a client whose requirements have genuinely become a different product, where a separate solution line is more honest than an extension that overrides most of the core.

We recommend the right solution - whether that's Microsoft Dynamics 365, Power Platform, or a custom-built CRM. Some businesses need the Microsoft ecosystem. Others need full control without licensing. We deliver both.

Can you send us the output of pac solution list?

Run it against production and against development and send us both. From those two lists we can usually tell you how far apart your environments have drifted, whether anything unmanaged has been imported where it should not have been, and what the shortest route to a reviewable release path looks like for your system. Solzet is a Dynamics 365 Customer Engagement and Power Platform consultancy in Yerevan, Armenia, working with clients and Microsoft partners across Europe and the US.