Skip to content

07 Deploy

RemyDuijkeren edited this page Jul 21, 2026 · 3 revisions

Deploy

flowline deploy packs the solution from the repo and imports it into a target environment. It is the promotion step — moving a tested solution from DEV through test, UAT, and into production.

flowline deploy test
flowline deploy uat
flowline deploy prod
flowline deploy https://contoso-staging.crm4.dynamics.com   # or a direct URL

The ALM loop

flowline push       # push code to DEV
flowline sync       # pull Maker Portal changes back to source
git commit
flowline deploy test
flowline deploy prod

deploy always operates from source control — it packs from the repo, not from DEV. Flowline blocks if there are uncommitted changes under the package folder (Solution/), any plugin project the solution file references, or the WebResources project file (exit code 12), or if local changes have not been synced to DEV yet (exit code 15 — run flowline sync first, or pass --force drift to skip the check).

What deploy does

Each step below has its own section further down, with the flag that skips it:

  1. Validate target — confirms the environment exists and its managed/unmanaged type matches what you're deploying.
  2. DTAP gate — the solution must already exist in the nearest predecessor environment, at an equal or higher version. (--skip-dtap-check)
  3. Pack the solution from Solution/src.
  4. Solution checker gate — blocks on any Critical finding. (--skip-solution-check)
  5. Pre-deploy backup of the target environment. (--no-backup)
  6. Orphan cleanup (pre-import) — reports, and for unmanaged solutions also deletes, components no longer in source. (--no-delete)
  7. Import — as a Dataverse Upgrade if the managed solution already exists in the target, otherwise a plain import. Plugin steps and workflows are always activated on import; unmanaged imports also publish all customization changes (see Managed vs unmanaged).
  8. Orphan cleanup (post-import) — retries anything deferred by a dependency in step 6.

Managed vs unmanaged

By default, deploy imports the unmanaged solution. Managed/unmanaged mode is configured exclusively via clone --managed/sync --managed, saved to .flowlinedeploy only ever reads it, never sets it:

flowline sync --managed   # opt into managed for this solution; sync --managed false reverts
flowline deploy prod      # uses whatever .flowline currently has configured

For managed deploys, deploy checks whether the solution already exists in the target and imports it as a Dataverse Upgrade when it does, so obsolete components get removed automatically. A first-time install stays a plain import instead — there's no prior version to upgrade from. See Further reading for why this matters and Orphan cleanup for what happens on unmanaged targets, where Upgrade doesn't apply.

Unmanaged imports also publish all customizations in the target environment (pac's --publish-changes), so UI-affecting changes like forms, views, and web resources take effect immediately. Managed deploys skip this — managed customizations always import already published.

First-import confirmation

The first time a solution is imported to a target that doesn't have it yet, deploy asks for confirmation, since Dataverse won't let you switch that target's managed/unmanaged mode later without an uninstall. Skip it non-interactively with --force first-import; it never fires once the solution already exists there.

DTAP gate

When .flowline defines multiple environment URLs, deploy enforces promotion order:

  • Dev is blocked. Use flowline push and flowline sync for the DEV environment.
  • Existence check. The solution must already exist in the nearest predecessor (e.g. Test before UAT, UAT before Prod). If it doesn't, deploy blocks.
  • Version check. The predecessor's installed version must be ≥ the local version. If it isn't, deploy blocks and shows both version numbers.
flowline deploy prod --skip-dtap-check   # bypass existence and version checks

A notice prints when the gate is skipped, identifying which predecessor was bypassed.

Solution checker gate

After packing, deploy runs pac solution check against the packed solution and blocks the deploy if any Critical-severity finding is reported. High/Medium/Low/Informational findings are reported but non-blocking.

The full report is written to artifacts/solution-check/ — check there if you want details beyond the terminal summary.

flowline deploy prod --skip-solution-check   # bypass the solution checker gate

Pre-deploy backup

After the solution checker gate passes, deploy takes a manual, on-demand backup of the target environment via pac admin backup before any pre-import Dataverse mutation. A backup failure aborts the deploy. This runs for every deploy target, not just Prod.

flowline deploy prod --no-backup   # bypass the pre-deploy backup

Orphan cleanup

Plain solution imports are additive — components removed from the local solution since the last deploy don't get removed by the import itself. For unmanaged deploys, Flowline diffs the target against local Solution.xml before import and closes that gap directly:

  • AUTO components (plugin assemblies, steps, web resources, custom APIs, workflows) — deleted if solo, or removed from the solution if shared with other solutions.
  • MANUAL components (tables, columns, forms, views, roles, connection references, bots) — listed in the report but never touched. Remove them via the Maker Portal.

Every orphan is classified into a risk tier, shown in the report:

Tier Meaning
Prio1 Blocks deployment — orphaned plugin assembly/type/step/step image
Prio2 Still running deleted logic — active workflow, enabled plugin step, callable custom API, in-use connection reference, published bot
Prio3 No functional impact if left — everything else. Just clutter: leaving them means a less clean environment, not a running problem.
flowline deploy test --no-delete   # print the orphan report without deleting anything

Managed deploys always print this report but never delete anything through it — components already installed as part of a managed solution reject ad-hoc delete calls; only Dataverse's own Upgrade/uninstall path can remove them. When the target already has the solution, that Upgrade runs as part of the import (see Managed vs unmanaged) and the report is a preview of what it's about to remove. On a first-time managed install, there's no Upgrade yet, so flagged components wait for a later redeploy to actually clear.

Components blocked by a dependency during pre-import cleanup are retried after the import completes; failures there are non-blocking and appear as warnings.

Artifacts

Packed solution zips are written to artifacts/. deploy reuses the last packed zip when nothing's changed since in the package folder, the plugin projects, or the WebResources project file — the same list that scopes the clean check, so the two can't disagree — so promoting through test → uat → prod builds it once — and prints whether it reused or repacked, and why, on every run.

On Azure Pipelines and GitHub Actions, the packed zip is surfaced automatically, no setup required: Azure Pipelines attaches it as a build artifact (named with the solution's version); GitHub Actions exposes its path as step output artifact-path-<SolutionName> for your own upload step:

- name: Deploy
  id: deploy
  run: flowline deploy prod

- name: Upload packed solution
  uses: actions/upload-artifact@v4
  with:
    name: ContosoSales
    path: ${{ steps.deploy.outputs.artifact-path-ContosoSales }}

If each DTAP stage runs on its own CI job, pass the artifact explicitly instead of relying on reuse:

flowline deploy uat --path artifacts/ContosoSales_unmanaged.zip

Options

Option Description
<target> prod, uat, test, dev, or a direct environment URL
--path <zip> Import this pre-built solution zip instead of packing from source
--no-delete Print the orphan report without executing deletions
--skip-dtap-check Skip existence and version promotion checks
--skip-solution-check Skip the solution checker gate
--no-backup Skip the pre-deploy environment backup

See 03-Command-Reference#deploy for the full option reference.

Why no solution mapping

pac solution pack's mapping file swaps in local build output at pack time instead of packing what sync captured from DEV. Flowline doesn't wire it up: Solution/src is what was pushed to DEV and exercised there, so deploy promotes what was tested, not what was built last. The full case is in 11-Philosophy#1-code-flows-up-configuration-flows-down--dev-is-the-verification-gate.

Keeping binaries out of git

If your goal is just to keep binaries out of git — the usual reason teams reach for --map — you don't need mapping to get it.

Two steps: gitignore the build artifacts, then copy them into Solution/src after building. The packed zip then carries your locally built output, with no binaries in git.

1. .gitignore:

# Plugin assembly (classic)
Solution/src/PluginAssemblies/**/*.dll

# Plugin package (NuGet)
Solution/src/pluginpackages/**/*.nupkg

# Web resource content — keep the .data.xml manifests
Solution/src/WebResources/**
!Solution/src/WebResources/**/
!Solution/src/WebResources/**/*.data.xml

2. Copy build output into Solution/src after building, before flowline deploy:

<plugin assembly .dll>   →  Solution/src/PluginAssemblies/<Assembly>/
<plugin package .nupkg>  →  Solution/src/pluginpackages/<Package>/package/
WebResources/dist/**     →  Solution/src/WebResources/<prefix>_<Solution>/

deploy packs what you copied in. Note this ships what pack time built, not what ran in DEV — the same trade --map makes — and it only covers changing existing components; a brand-new step or web resource still needs push + sync to create its manifest.

Further reading

Clone this wiki locally