Removed
PreImageAttribute.Name/PostImageAttribute.Name: get-only duplicates ofAliasthat nothing read. C# forbids setting a read-only property as a named attribute argument, so they could never be assigned in[PreImage(...)]/[PostImage(...)], and the metadata scanner only ever readAlias. Dataverse's imagenamefield is unaffected — it's still written on every registration, defaulting to"Pre Image"/"Post Image"and followingAliaswhen set. Only breaks code that read.Nameoff an attribute instance; substitute.Alias, which returns the identical value.HandlesAttribute.IsCustomMessage: never read by anything. It was assigned in the constructor body, and the metadata scanner reads attributes viaCustomAttributeData(constructor arguments only) — so a ctor-body assignment was never visible to it in the first place. Distinguishing a Custom API message from a built-in one is done by inspecting the constructor argument's type instead, which is unaffected. Only breaks code that read.IsCustomMessageoff an attribute instance.- Unused package references:
Microsoft.Extensions.Logging.Console(noAddConsoleanywhere — logging goes through Serilog) and threePackageVersionentries pinning nothing (Microsoft.CrmSdk.CoreAssemblies,Microsoft.CodeAnalysis.CSharp,Microsoft.CodeAnalysis.Analyzers). No behavior change.
Added
-
deploy --dry-run: runs every pre-deploy check — DTAP gate, git-clean, local plugin/web-resource drift, packing, the solution checker gate, and the orphan-cleanup report — plus a labeled environment backup (flowline-dryrun-..., distinct from a real deploy'sflowline-deploy-...), then stops before importing. Never callspac solution import, never runs post-import cleanup, and never publishes a CI artifact-publish signal. The first-import confirmation prompt becomes an informational note instead of a block, since dry-run never performs the irreversible action it guards. -
Multiple plugin projects per solution:
pushnow discovers plugin projects by reading the solution file and reflecting each candidate's build output forIPlugin/CodeActivitytypes, rather than assuming one project namedPluginsproducing an assembly namedPluginsatbin/Release/net462/publish/. Each discovered project registers independently — its own assembly, plugin types, steps, and Custom APIs — in a singlepush. Project names, folder locations, a custom<AssemblyName>, and non-standard build output paths all work as long as the solution file references the project. A project whose build output reflects cleanly and bears neither type is skipped;--verbosereports what was skipped and why.PluginPackageModeremains one setting per solution. -
clonealso scaffoldsCLAUDE.md:AGENTS.mdalone isn't picked up by Claude Code, which only auto-loadsCLAUDE.md— clone now writes a one-lineCLAUDE.md(@AGENTS.md) alongside it so Claude Code loads the same instructions. Skipped ifCLAUDE.mdalready exists. -
flowline sln add <path>: wires a.cdsprojinto the project's solution file.dotnet sln addrefuses a.cdsproj— and exits 0 while doing it, so a script can't detect the failure (dotnet/sdk#47638) — so Flowline writes the entry itself.clonealready does this for projects it creates; the command exists for the ones it didn't, like a repo migrating off spkl, Daxif, or PACX. Runs standalone: no.flowline, no git repo, no PAC login. Works with.slnand.slnx, finds the solution file in the current directory (matching plaindotnet sln add's own scoping — it does not walk upward), and never converts an existing file. It won't create a solution file — with none in the current directory it exits3and points atdotnet new sln. -
Guards the active PAC auth profile before every Dataverse call:
clone,deploy,drift,generate,provision,push, andsyncnow check that PAC CLI's currently activepac authprofile actually matches the target environment before doing anything — previously a mismatched profile connected silently as whatever identitypachappened to have selected. Interactively, a mismatch shows the active and target profile side by side and prompts to switch (default no); declining, or running non-interactively, fails with apac auth select --name/--index ...remediation message. Pass--auto-select-auth-profileto switch automatically without a prompt.deployalso guards the DTAP predecessor environment's profile before its ownpac.exe solution-listprobe.flowline statusreports a profile mismatch too, but only as an advisory note — it never blocks. User-facing text now says "PAC auth profile" throughout, not "PAC profile". Breaking for CI: a non-interactive run against an environment whose profile isn't currently active now fails instead of silently connecting as the wrong identity — add--auto-select-auth-profileto existing pipelines that switch environments without also switchingpac auth selectfirst. -
Two-tier fallback for a web resource file with no recognizable type: a local file with no extension, or an unrecognized one, used to leave
push/syncunable to determine its Dataversewebresourcetypeat all. Tier 1 resolves it from an existing Dataverse record — checking this solution's own records first, then any other solution's, since a match there means the file is really an update to a record this solution doesn't own. Tier 2 falls back to sniffing the file's own bytes: magic-byte checks for PNG/JPG/GIF/ICO, and for text files a schema/doctype/syntax check for RESX, HTML, and JavaScript — only signals at 90%+ confidence count, so the weaker SVG/XML/CSS heuristics stay disabled. A file neither tier can resolve still fails the push, now with a clear "metadata lookup and content sniffing were both tried and neither resolved a type" message instead of a generic error.
Changed
-
clonescaffolds a.slnxby default: the .NET 10 default format, replacing the classic.sln. Flowline previously opted out on the assumption that a.slnxcan't hold a.cdsproj— it can (verified on SDK 10.0.302:dotnet sln listenumerates the entry anddotnet buildruns SolutionPackager through to the zip). Onlydotnet sln addrefuses a.cdsproj, and Flowline writes that entry itself now. Existing projects are untouched — Flowline reads both formats and never converts one. A project that already has a.slnkeeps it — clone writes into whatever solution file is there, and only creates one when there is none. -
Exactly one Dataverse solution per project, always at the root (BREAKING CHANGE!): Flowline no longer supports multiple solutions in one project.
.flowline'sSolutionsarray collapses to a singleSolutionobject (plus a requiredSchemaVersion);deploy,sync, anddriftdrop their solution-name selector entirely, andpush/generate's selector now validates against the configured solution instead of picking among several. An old multi-solution.flowlinefails fast on load, naming the fix: delete the project's config and itssolutions/<Name>/folder, thenflowline clone <solution>again — there's no auto-migration. Alongside this, theSolution/,Plugins/, andWebResources/projects are now located by reading the.slnx/.slnfile itself instead of assuming fixed folder names — renaming or moving any of them no longer breaksclone/push/sync/deploy, andclonenow recognizes a project that's already registered even if it moved. A project with no WebResources project is no longer a hard error:push,sync, anddeploynow warn and skip the web-resource side instead of blocking (a genuine ambiguous match between two candidate projects still fails). A PowerApps Component Framework project sitting alongside the real WebResources project is no longer misdetected as it. -
pushno longer requires a plugin project on a WebResources-only solution: under the default scope, zero discovered plugin projects is a valid, common case — it only failed with "No plugin project found" when the user explicitly asked for one via--scope plugins/--scope assemblyonly.
Fixed
-
pushrefuses to guess a project out of discovery: the discovered project set also decides what the orphan sweeps treat as having local source, so a project silently dropped from discovery had its live assembly, steps, and Custom APIs deleted — and the Custom API sweep isn't gated by--force. Dropping is now allowed only when it's certain. A project whose build output reflects cleanly and carries noIPlugin/CodeActivitytype is still skipped silently (the common case: WebResources, test projects, shared libraries). A project Flowline couldn't classify fails the push instead, naming the project: when no assembly in its output could be reflected at all (usuallyMicrosoft.Xrm.Sdk.dllisn't copy-local, which hides a real plugin project's base types), or when--no-buildleft nothing inbin/Releaseto reflect. The cheap csproj-text pre-filter now defers to reflection whenever it can't be confident — aProjectReferenceor aDirectory.Build.propsabove the project means the SDK reference may never appear in the csproj itself. Standalone mode (--pluginFile) runs no discovery and is unaffected. -
pushonly deletes a Custom API it can prove is its own: the Custom API sweep treated "this API's plugin type isn't one I recognise" as "this API is orphaned" and deleted it as part of the normal plan, no--force delete-orphansinvolved. Two things made that wrong. A Custom API references a plugin type as its implementation (customapi.plugintypeidpoints down), unlike a step, which is owned by its type — so losing an implementation doesn't orphan the contract. And a Custom API has no assembly-shaped handle, so Flowline can only find them publisher-wide: the sweep's input was every Custom API the publisher ever created, including other projects' and other repos'. A Custom API is public API surface with external callers, so the bar to delete one is now higher than for a step, not lower.What changes: an API whose plugin type belongs to this push and is no longer declared in source is still deleted on a normal push — unchanged. An API whose plugin type isn't one this push owns is never deleted, with or without
--force;--verbosesays why it was left. An API with noplugintypeidat all — which may be a contract waiting for an implementation — now needs--force delete-orphans, the same gate the orphan assembly and step sweeps use. This supersedes the earlier fix that protected a sibling project's Custom APIs by feeding every pushed project's plugin type ids into the planner; that widening is removed, since under positive attribution there is no "unknown" bucket to shrink. Single-project and standalone pushes see the same protection. -
pushnow protects sibling projects on the plugin-package (.nupkg) path too: the package push path had its own unlinked-Custom-API sweep that didn't get the sibling-assembly protection the classic.dllpath above already has, so a.nupkgproject pushed alongside a.dllproject in the same command could delete the.dllproject's Custom APIs as unowned orphans — ungated by--force. The package path now feeds sibling plugin-type ids into the sweep the same way, and reflects once at prepare time so a multi-assembly package's non-primary assemblies are visible to it. -
pushrejects a raw assembly already registered as part of a plugin package: pushing a.dlldirectly (--scope assemblyonly) when Dataverse already has it registered under aPluginPackageused to silently override the package-owned assembly. It now fails with a clear message to push the.nupkginstead — automated migration isn't supported. The same push also detects a secondary DLL in a multi-assembly package that's already registered classic (non-package) in Dataverse, and fails with a clear message instead of the earlier confusing failure at content-write time. -
Orphan cleanup no longer misclassifies a reconciled Role as orphaned: Role was the only orphan-detected type still matched purely by the raw id declared in Solution.xml. Dataverse reconciles security roles by name on import when a role of that name already exists in the target, so a role synced from one environment could carry a different live id in another — the raw id-match alone would then report a still-assigned role as removable. Role is now also resolved live by name (its local
Roles/<name>.xmlfile name), the same way WebResource/Entity/OptionSet already are, in addition to the existing id-match. -
pushno longer prints a raw stack trace for a bad[CustomApi]/[Step]/etc. attribute: every validation errorPluginTypeMetadataScannerraises while reading a plugin assembly (invalid[CustomApi(UniqueName = ...)]format, a bad secondary table, and about 20 other checks) used a plainInvalidOperationException, whichProgram.cs's global handler doesn't recognize — it fell through to a full raw exception dump instead of a cleanError: ...line. Both places that analyze a plugin assembly (push's normal and--scope assemblyonlypaths) now catch that exception and rewrap it as aFlowlineException, so any misconfigured attribute reads as a one-line validation error like every other rejected push. -
Connects via the Windows account broker (WAM) for PAC's "OperatingSystem"-type profiles: PAC CLI 2.9+ defaults a new profile to storing its refresh token in the Windows account broker rather than PAC's own file-based token cache, which Flowline read exclusively — those profiles failed with a session-expired error even though
pacitself still worked. Flowline now tries the WAM broker first on Windows, then the OS account, then falls back to the file cache. -
No longer connects using a sample/tutorial Azure AD app id: Flowline used Microsoft's own "Dynamics 365 Example Client Application" sample app id, which lacks admin consent in most tenants and could fail with
AADSTS90072even thoughpac's own calls worked. It now uses PAC CLI's real registered app id. -
Picks the cached sign-in by tenant, not by username alone: the same email cached under more than one tenant — e.g. a stale guest account alongside the real member account — could resolve to the wrong one and fail with
AADSTS90072("account doesn't exist in tenant"). The cache lookup now prefers the account whose tenant matches the profile's own, falling back to username-only and then any account. -
No longer tells users to authenticate against an internal BAP admin URL: a session-expired error used to suggest
pac auth create --url https://api.bap.microsoft.com, an internal admin endpoint rather than a real Dataverse environment. Environment lookups now use the target environment's own URL throughout, so the remediation message points at something a user can actually authenticate against. -
sync's dirty-check no longer ignores changes it can't map to a single component: files underSolution/src/Other/(Solution.xml,Customizations.xml) have no one-to-one component mapping, so they were excluded from the dirty-tree file count entirely — a change there could silently bypass the safety gate that blocks syncing over uncommitted work. They're now counted toward the total; only the displayed per-file breakdown still depends on whether the path resolves to a component. -
driftno longer false-flags a dotted classic plugin assembly as orphaned: PAC's solution-unpack strips periods from a classic PluginAssembly's on-disk filename (Cr07982.LegacyPluginsunpacks toCr07982LegacyPlugins.dll), while build output keeps the real, dotted name — a raw filename comparison misreported any dotted classic assembly as orphaned, risking its accidental deletion. Drift now resolves the assembly's true name from its companion.dll.data.xmlmetadata before comparing. NupkgPluginPackageexports, which don't strip the dot, and any DLL with no readable metadata are unaffected. -
deploy --pathno longer requires a solution file to be present: it always resolved the solution's package folder from the.slnx/.slnfile even when the deploy source was an already-built zip and didn't need it, so a--pathdeploy from an artifact-only checkout (no solution file in the repo) failed before it started. The solution file is now only resolved on the paths that actually need it (packing, drift, version checks).