9.1A solution in source control
from a zip of four files to a tree of thirty-two
A solution is a row in Dataverse and an export of it is a zip, and your reviewer can read neither. What goes into source control is the unpacked form: one file per component, in a directory tree whose shape is stable across exports, so a change to one form is a change to one file.
Start from the environment's own list, which is shorter than the table behind it.
An export is taken twice, once each way, because the two are different files with different jobs. Chapter 3 sets out which is which (§3.2); what matters here is what comes out of the zip.
Evidence — the list, the two exports, and the tree the unpack wrote
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Listing all Solutions from the current Dataverse organization... Unique Name Friendly Name Version Managed ch23lab ch23lab 1.0.1.0 True ch45Lab ch45Lab 1.0 False ch45WrOnly ch45WrOnly 1.0 False ch89alm ch89alm 1.0.0.0 False Cra969a Common Data Services Default Solution 1.0.0.0 False Default Default Solution 1.0 False
ch23lab, ch45Lab and ch45WrOnly appear here because the command listed them.pac solution list reported 6 solutions in skylib-test. A query against the solutions table in the same environment on the same day reported 448 rows. The command shows what a maker would recognise; the table holds the first-party solutions underneath it as well.verifiedartifact: ch09-solution-list-test · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Starting Solution Export... Solution export succeeded. Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Starting Solution Export... Solution export succeeded.
--managed flag. The solution, the environment and the version are the same in both, and the pair produces two files.Each exported zip holds four members: customizations.xml, solution.xml, [Content_Types].xml, and environmentvariabledefinitions/ch89_ApiBaseUrl/environmentvariabledefinition.xml. The environment variable definition rides in the zip as its own file beside the monolithic customizations.xml. Between the lab's two exports the four members are the same set and customizations.xml differs by 100 bytes.verifiedartifact: ch09-export-both · artifacts-ch09.md
Unpacking Solution... Extracting .../lab89/work/out/ch89alm.zip to .../lab89/work/unpacked Skipping localization Processing Component: Entities - ch89_Note - ch89_Release Processing Component: EntityRelationships Unmanaged Extract complete. Unpacked Solution.
The unpack turned a four-member zip into 32 files. Each of the two tables received an Entity.xml, a RibbonDiff.xml, three files under FormXml/ and seven under SavedQueries/ — views the platform generated, which the solution project never asked for and which arrive in source control with it. The customizations.xml of 148,491 bytes became a per-component tree, and the Other/Customizations.xml left behind is 531 bytes. The alternate key on ch89_Release has no file of its own: it is an EntityKeys element inside that table's Entity.xml.verifiedartifact: ch09-unpack-tree · artifacts-ch09.md
<environmentvariabledefinition schemaname="ch89_ApiBaseUrl">
<defaultvalue>https://api.dev.example/</defaultvalue>
<displayname default="API base URL">
<label description="API base URL" languagecode="1033" />
</displayname>
<introducedversion>1.0.0.0</introducedversion>
<iscustomizable>1</iscustomizable>
<isrequired>0</isrequired>
<secretstore>0</secretstore>
<type>100000000</type>
</environmentvariabledefinition>
The unpacked environment variable definition has seven child elements, and not one of them holds a value for a target environment. The default it does carry is the value the source environment was using when the export was taken.verifiedartifact: ch09-envvar-definition · artifacts-ch09.md
9.1.1Clone compared with unpack
Two commands give you a directory from a solution, and the directories differ. pac solution unpack takes a zip you already have. pac solution clone starts from the environment.
The behaviour underneath both verbs is SolutionPackager's, which reversibly decomposes a compressed solution file into XML files and other files so that a source control system can manage them. The documentation states that the tool is no longer the recommended way to unpack and pack: its capabilities are incorporated into the Power Platform CLI, and pac solution carries them as unpack, pack, clone and sync. The packagetype argument takes Unmanaged, Managed or Both, and defaults to Unmanaged.citedsource: Power Platform ALM docs — SolutionPackager › what the tool does and the packagetype argument · artifacts-ch09.md
There are two folder layouts, and the tree above is the older one. The XML format keeps solution metadata in Other\Solution.xml and Other\Customizations.xml with component files in a flat hierarchy beside them, and it is what an extract produces without further configuration. The YAML source control format, introduced alongside Dataverse Git integration, distributes YAML files across a structured hierarchy; the documentation gives it cleaner per-component diffs, and canvas app .msapp files and modern flows are carried in that format alone. The format is detected by the presence of a solutions/ subfolder holding *solution.yml files.citedsource: Power Platform ALM docs — SolutionPackager › the YAML source control format · artifacts-ch09.md
Evidence — what a clone wrote that an unpack did not
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Starting Solution Export... Unmanaged solution export succeeded. Starting Solution Export... Managed solution export succeeded. Unpacking Solution... Extracting /tmp/ch89alm.zip and /tmp/ch89alm_managed.zip to .../lab89/work/cloned/ch89alm/src… both extracts, in the shape the unpack listing above shows …Unpacked Solution. Solution clone extract succeeded. Dataverse solution project with name 'ch89alm' created successfully in: '.../lab89/work/cloned'
The clone wrote 40 files where the unpack of the same solution wrote 32. Under src/ it wrote 38, and the six beyond the unpack's tree are the _managed.xml counterparts of the six form files — the managed form of each form, extracted beside the unmanaged one. The two files outside src/ are ch89alm.cdsproj and .gitignore. Every other path under src/ matches the unpacked tree.verifiedartifact: ch09-clone-tree · artifacts-ch09.md
Guidance
Clone once, at the start, to get the project and the two-package tree. Unpack on every build after that, from a zip the build itself exported. A clone in a build script goes back to the environment for a fresh export each time, which makes the artifact under review a different artifact from the one that was tested.
9.2Reading the diff, and the conversion the packer refuses
what an edit looks like, and where the round trip has to go through a server
The tree exists so someone can read your edit. One file changed by a few lines is reviewable; the same edit inside a hundred-and-fifty-kilobyte customizations.xml is not.
Your edited tree then has to become a zip again, and which direction you can pack it in is not a free choice.
The packer processes a compressed solution of either type and cannot convert one type to the other. The documented route from unmanaged to managed runs through a server: import the unmanaged zip into a Dataverse environment and export the solution from there as managed.citedsource: Power Platform ALM docs — SolutionPackager › managed and unmanaged cannot be converted offline · artifacts-ch09.md
Evidence — the two edits as a diff, the refusal, and the round trip back in
@@ -6,7 +6,7 @@
<LocalizedName description="ch89alm" languagecode="1033" />
</LocalizedNames>
<Descriptions />
- <Version>1.0.0.0</Version>
+ <Version>1.0.1.0</Version>
<Managed>0</Managed>
<Publisher>
<UniqueName>ch89alm</UniqueName>
@@ -1,5 +1,5 @@
<environmentvariabledefinition schemaname="ch89_ApiBaseUrl">
- <defaultvalue>https://api.dev.example/</defaultvalue>
+ <defaultvalue>https://api.test.example/</defaultvalue>
<displayname default="API base URL">
<label description="API base URL" languagecode="1033" />
</displayname>
Two edits were made to the unpacked tree by hand: the solution version from 1.0.0.0 to 1.0.1.0 in Other/Solution.xml, and the environment variable's default value in its own file. Each is one changed line in the diff between the edited tree and the tree the unpack produced.verifiedartifact: ch09-edit-diff · artifacts-ch09.md
Packing Solution... Packing .../lab89/work/unpacked to .../lab89/work/out/ch89alm_1_0_1_0_managed.zip Microsoft PowerPlatform CLI Version: 2.10.1+g52c3983 (.NET 10.0.10) Online documentation: https://aka.ms/PowerPlatformCLI Feedback, Suggestions, Issues: https://github.com/microsoft/powerplatform-build-tools/discussions Error: Solution package type did not match requested type. Command line argument: Managed Package type: Unmanaged Packing Solution... Packing .../lab89/work/unpacked to .../lab89/work/out/ch89alm_1_0_1_0.zip Processing Component: Entities - ch89_Release - ch89_Note… ten further component headings, then Processing Sharded Component Files …Unmanaged Pack complete. Packed Solution.
Packing a folder extracted as unmanaged into a managed zip failed by name, reporting the requested type and the type it found. The same folder packed as unmanaged succeeded, and the edited default value survived into the packed zip. A build that assumes a folder can be packed either way discovers otherwise at the pack step.verifiedartifact: ch09-pack-refusal · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Solution Importing... Solution Imported successfully. Publishing All Customizations... Published All Customizations.
The zip packed from the edited tree imported into skylib-test unmanaged and published in one command. The round trip is export, unpack, edit, pack, import — and it lands back in the environment it started from, because that is the only place the managed form can be produced.verifiedartifact: ch09-import-test · artifacts-ch09.md
Caution
A build that produces a managed artifact needs a Dataverse environment in the loop, and that environment's state is an input to the build. Two builds of the same commit, taken either side of somebody publishing a change in that environment, produce different managed zips.
Guidance
Keep a build environment reserved for the build. If the environment producing your managed export is also the one makers work in, pin the export to a commit by importing the unpacked tree into it first, as the run above does.
9.3The deployment settings file
the values a solution deliberately does not carry
An environment variable and a connection reference exist so that a value belonging to one environment is named in the solution and supplied at import. Chapter 3 sets out what each one is and how the definition and the value split between managed and unmanaged layers (§3.6). This section is the other half: the file that supplies them, and what it looks like when it is not empty.
After importing a solution carrying connection reference and environment variable information, a person is prompted for the values in the interface, which the documentation says does not work well for a fully automated deployment. The deployment settings file is the JSON that pre-populates them, passed as a parameter at import, and the documentation says it can be kept in the source control system and managed there. It is generated by pac solution create-settings --solution-zip <solution_zip_file_path> --settings-file <settings_file_name>. In the documented example an environment variable entry carries SchemaName and Value, and a connection reference entry carries LogicalName, ConnectionId and ConnectorId. At import, connection references are validated so that the connections placed in them will be usable by the owner of the reference.citedsource: Power Platform ALM docs — deployment settings file › shape and how it is passed · artifacts-ch09.md
Evidence — the settings file, the import that used it, and the value it left
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Starting Solution Export... Solution export succeeded. Extracting Dataverse connection references and/or environment variables from: .../lab89/work/out/ch89alm_managed_1_0_1_0.zip Deployment settings file created: .../lab89/work/out/ch89alm.settings.json
pac solution create-settings read the lab's managed export and wrote a file with three top-level keys — EnvironmentVariables, ConnectionReferences and CopilotAgents — of which the first held one entry and the other two were empty. That entry carries six fields where the documented example carries two: beside SchemaName and Value this version of the CLI writes DefaultValue, Name, TypeId and IsRequired. Value arrives empty; the default carried in the solution is reported beside it and is not used to fill it.verifiedartifact: ch09-create-settings · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-prod Solution Importing... Solution Imported successfully. Publishing All Customizations... Published All Customizations.
The managed export reached skylib-prod in one command, with the target's environment variable value supplied by the settings file. The lab's development environment took no part: the source was skylib-test throughout.verifiedartifact: ch09-import-prod · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-prod schemaname defaultvalue environmentvariabledefinitionid v.value ch89_ApiBaseUrl https://api.test.example/ 0fea51c7-982d-4769-a072-0b7b2a70422e https://api.prod.example/ Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test schemaname defaultvalue environmentvariabledefinitionid ch89_ApiBaseUrl https://api.test.example/ 0fea51c7-982d-4769-a072-0b7b2a70422e
v.value column in the second block is the join finding no value row.After the import, skylib-prod holds a definition whose default is the value that shipped in the solution and a separate value record holding https://api.prod.example/. skylib-test holds the definition and no value record at all — the same FetchXML, with an outer join to the value table, returned no v.value column there. The definition is what the solution carried; the value is what the settings file supplied.verifiedartifact: ch09-envvar-readback · artifacts-ch09.md
Guidance
Generate the settings file from the zip at each build and keep the values per environment beside it. Read the value back from the target after the import, the way this run does: an import that left a variable unsupplied and one that supplied the right thing both report success.
9.4Update, upgrade and patch
what happens to a component the new version dropped
You replace a managed solution already installed downstream by importing a higher version of it. Your import action decides what happens to a component the previous version carried and the new one does not, and the three actions differ on exactly that point.
Upgrade is the default. It rolls up previous patches in one step, and any component associated with the previous version that is absent from the newer version is deleted. Stage for upgrade raises the version but defers that deletion until the upgrade is applied later, and is documented for the case where the old and new solutions have to be installed concurrently so that data can be migrated first. Update replaces the solution and leaves the components the newer version omits in place; the documentation gives it the best performance of the three, typically finishing in less time than the upgrade methods.citedsource: Power Apps docs — Update a solution › the three solution action options · artifacts-ch09.md
A version has the form major.minor.build.revision, and an update needs a higher value in one of those four positions than the parent solution. The documented and recommended way to remove a managed component from an environment is the upgrade the lab ran: take the component out of the solution where it originated, export as managed, and import the result as an upgrade — which removes the component where nothing else in the target depends on it.citedsource: Power Apps docs — Update a solution › version numbers and removing a managed component · artifacts-ch09.md
The documentation describes a staged upgrade as leaving the original solution installed beside a second one suffixed _Upgrade, completed later by applying the upgrade. It then notes that recent platform changes have optimised the single-step upgrade to use neither a temporary _Upgrade solution nor an uninstall of the original.citedsource: Power Apps docs — Update a solution › completing a staged upgrade · artifacts-ch09.md
The lab ran the two that differ in outcome, against the same target, with the same component dropped. The component comes out of the solution in the source environment first.
Evidence — one component dropped, then the same target taking an update and an upgrade
HTTP 200
{"@odata.context":"https://org5014704c.crm.dynamics.com/api/data/v9.2/$metadata#Microsoft.Dynamics.CRM.RemoveSolutionComponentResponse","id":"bca9c90b-9090-f111-8076-70a8a59af8d5"}
Before the removal the solution carried three root components: two tables at component type 1 and the environment variable definition at component type 380. Removing ch89_Note from the solution left the table in skylib-test and took it out of what the next export carries.verifiedartifact: ch09-remove-component · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-test Starting Solution Export... Solution export succeeded.
Dropping one of the two tables took the managed export from 13,164 bytes at 1.0.1.0 to 8,126 bytes at 1.1.0.0. The size of the artifact is a signal that the component list changed, and it changed in the source environment before any import ran.verifiedartifact: ch09-version-bump · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-prod Solution Importing... Solution Imported successfully. Publishing All Customizations... Published All Customizations. Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-prod No results returned.
No results returned. is what the CLI prints for a table that exists and holds no rows.Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-prod Solution Importing... Solution Imported successfully. Publishing All Customizations... Published All Customizations. Error: The entity with a name = 'ch89_note' with namemapping = 'Logical' was not found in the MetadataCache.LazyDynamicMetadataCache with version 2025300 and timestamp 2025300 Connected as skydude@itrancyngergmail.onmicrosoft.com Connected to... skylib-prod No results returned.
Importing 1.1.0.0 into skylib-prod with no upgrade flag left the dropped table in the target. The solution list afterwards reported ch89alm at 1.1.0.0 and managed, and a query against ch89_note answered No results returned. — the response for a table that exists and is empty. A control query in the same environment on the same day, against a table that had never existed, answered with an error naming the logical name instead.verifiedartifact: ch09-import-prod-update · artifacts-ch09.md
Importing 1.2.0.0 into the same target with --stage-and-upgrade removed the table. The query that had answered after the update now failed with the metadata-cache error, while the table the solution still carries answered in the same run. No ch89alm_Upgrade row remained in the solution list afterwards, which was reported at 1.2.0.0 and managed.verifiedartifact: ch09-import-prod-upgrade · artifacts-ch09.md
Caution
The two imports differ by one flag and by what is left in the target afterwards. A release process that has always passed the flag and a release process that has never passed it both look like they work, and the difference between them shows up the first time a component is dropped — in the target, as a table that no release removed and that still holds its rows.
Guidance
Choose the import action once, per environment, and write it into the pipeline rather than into a runbook. Where a target has to keep a component the newer version dropped, write a comment beside the flag saying so, because the next reader will assume the omission was an oversight.
9.5Pipelines, Azure DevOps and the service principal
the product, the alternatives, and the identity each of them needs
Power Platform Pipelines is a feature of the platform: an admin configures stages between environments, and a maker deploys from inside the development environment. Azure DevOps and GitHub Actions are your other route, where the same steps are tasks in a workflow file. The lab has no pipelines host and no build system pointed at it, so both routes are cited here. What it could run is the identity each of the three depends on.
The licensing condition is precise and it is what decides whether a tenant can use the feature. Developer environments are not required to be managed environments and can be used for development and testing on the developer plan. The pipelines host should be a production environment and does not itself have to be a managed environment. Every other environment used in a pipeline must be enabled as a managed environment, and licences granting premium use rights are required for all managed environments. The documentation also states that from February 2026 Microsoft begins enabling managed environments for pipeline target environments that are not already enabled.citedsource: Power Platform ALM docs — Pipelines › which environments must be managed · artifacts-ch09.md
A pipeline deploys solutions together with configuration for the target — connections, connection references and environment variables — and carries no data stored in Dataverse tables. Four documented boundaries decide whether it fits a given team. It does not deploy unmanaged solutions. It offers no choice of import action: the default behaviour is upgrade without overwrite customizations, and nothing else is available. It does not deploy across tenants, and the documentation recommends Azure DevOps or GitHub for that. It deploys one solution per request. The solution is exported at the moment the maker requests the deployment, and the same artifact travels through the later stages, which is what stops a customization bypassing a stage.citedsource: Power Platform ALM docs — Pipelines › what a pipeline deploys and what it will not do · artifacts-ch09.md
GitHub Actions for Microsoft Power Platform is a collection of platform-specific actions published at github.com/marketplace/actions/powerplatform-actions, which remove the need to download tooling and scripts by hand. Two connection types are documented: username and password, which does not support multifactor authentication, and service principal with a client secret. The actions run on Windows agents and on Linux agents.citedsource: Power Platform ALM docs — GitHub Actions for Power Platform › what they are and how they authenticate · artifacts-ch09.md
The Azure DevOps equivalent is Power Platform Build Tools, whose tasks fall into four categories: helper, quality check, solution, and environment management. Version 1.0 is built on PowerShell and version 2.0 on the Power Platform CLI, and 2.0 is the version being serviced. The documentation states that the 2.0 tasks are the same tasks the GitHub actions use, which is why the two routes behave alike. Its documented way to create the identity is pac admin create-service-principal, which registers an application object and a service principal name in Microsoft Entra ID and then adds the application as an administrator user to the tenant. Its role parameter is optional and defaults to System Administrator, so an invocation that does not pass one grants that role to the application user. On success four columns are documented, one of which is the client secret in clear text, and the documentation warns that the secret cannot be retrieved again once the prompt is cleared.citedsource: Power Platform ALM docs — Build Tools for Azure DevOps › tasks, versions and the service principal · artifacts-ch09.md
A CI identity is created once and reused for longer than the pipeline it was created for. It is granted the default role at the moment of creation, and in most estates nothing later narrows it. Its secret expires on a date nobody owns: a release that fails on an expired secret fails at the deployment step with an authentication error, which reads as an outage rather than as a renewal.experientialbasis: tenants where the first CI identity was created against one environment and later reused, and where the secret's expiry was the thing nobody had a reminder for until a release failed on it
Evidence — the pipeline surface the lab could reach, and the identity it registered
Connected as skydude@itrancyngergmail.onmicrosoft.com Microsoft PowerPlatform CLI Version: 2.10.1+g52c3983 (.NET 10.0.10) Online documentation: https://aka.ms/PowerPlatformCLI Feedback, Suggestions, Issues: https://github.com/microsoft/powerplatform-build-tools/discussions Error: Resource not found for the segment 'deploymentpipelines'.
pac pipeline list, run against skylib-test, failed with Resource not found for the segment 'deploymentpipelines'. deploymentpipelines is a table the pipelines host solution installs, so an environment not associated with a host does not have it. The lab meets the feature as an absent table rather than as a licence refusal.verifiedartifact: ch09-pipeline-list · artifacts-ch09.md
Connected as skydude@itrancyngergmail.onmicrosoft.com Creating Entra ID Application 'ch89-ci'... Done Creating Entra ID Service Principal... Done Connected to... skylib-test Registering Application 'bde22820-badb-4c02-948b-765540cb1d08' with Dataverse... Done Creating Dataverse system user and assigning role... Done Application Name ch89-ci Tenant Id 03cc7adf-0a88-42ef-bdaf-6ff598d14b45 Application Id bde22820-badb-4c02-948b-765540cb1d08 Service Principal Id 52256121-4241-46c2-9ce5-926fbaa6b019 Client Secret <redacted> Client Secret Expiration 8/5/2027 5:27:30 AM +00:00 System User Id bcc43b5c-8e90-f111-8076-70a8a59af8d5 Connected as skydude@itrancyngergmail.onmicrosoft.com App Id Secret Expiry Name bde22820-badb-4c02-948b-765540cb1d08 2027-08-05 05:27:30Z ch89-ci
One command registered an Entra ID application, created its service principal, registered the application with Dataverse and created an application user in skylib-test. The capture reports that a role was assigned and does not name it; which role that is, is the documented default cited above. The output reported seven labelled fields where the documentation describes four columns, adding the application name, the service principal id and the system user id. pac admin list-service-principal had printed a header and no rows before the create and reported the application afterwards.verifiedartifact: ch09-service-principal · artifacts-ch09.md
Guidance
Choose between the product and the workflow file on the licence, then on the team. Where every target is already a managed environment, Pipelines removes a build system from the estate. Where they are not, the cost of making them managed is the real comparison, and Azure DevOps or GitHub Actions reach the same environments with the same CLI underneath.
Give the application user the least role its tasks need, at the moment you create it. Put the secret's expiry date in the calendar that already holds your certificate renewals.