Skip to content

0.13.0

Latest

Choose a tag to compare

@github-actions github-actions released this 24 Jul 09:45

Removed

  • PreImageAttribute.Name / PostImageAttribute.Name: get-only duplicates of Alias that 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 read Alias. Dataverse's image name field is unaffected — it's still written on every registration, defaulting to "Pre Image"/"Post Image" and following Alias when set. Only breaks code that read .Name off 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 via CustomAttributeData (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 .IsCustomMessage off an attribute instance.
  • Unused package references: Microsoft.Extensions.Logging.Console (no AddConsole anywhere — logging goes through Serilog) and three PackageVersion entries 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's flowline-deploy-...), then stops before importing. Never calls pac 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: push now discovers plugin projects by reading the solution file and reflecting each candidate's build output for IPlugin/CodeActivity types, rather than assuming one project named Plugins producing an assembly named Plugins at bin/Release/net462/publish/. Each discovered project registers independently — its own assembly, plugin types, steps, and Custom APIs — in a single push. 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; --verbose reports what was skipped and why. PluginPackageMode remains one setting per solution.

  • clone also scaffolds CLAUDE.md: AGENTS.md alone isn't picked up by Claude Code, which only auto-loads CLAUDE.md — clone now writes a one-line CLAUDE.md (@AGENTS.md) alongside it so Claude Code loads the same instructions. Skipped if CLAUDE.md already exists.

  • flowline sln add <path>: wires a .cdsproj into the project's solution file. dotnet sln add refuses 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. clone already 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 .sln and .slnx, finds the solution file in the current directory (matching plain dotnet 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 exits 3 and points at dotnet new sln.

  • Guards the active PAC auth profile before every Dataverse call: clone, deploy, drift, generate, provision, push, and sync now check that PAC CLI's currently active pac auth profile actually matches the target environment before doing anything — previously a mismatched profile connected silently as whatever identity pac happened 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 a pac auth select --name/--index ... remediation message. Pass --auto-select-auth-profile to switch automatically without a prompt. deploy also guards the DTAP predecessor environment's profile before its own pac.exe solution-list probe. flowline status reports 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-profile to existing pipelines that switch environments without also switching pac auth select first.

  • 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/sync unable to determine its Dataverse webresourcetype at 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

  • clone scaffolds a .slnx by default: the .NET 10 default format, replacing the classic .sln. Flowline previously opted out on the assumption that a .slnx can't hold a .cdsproj — it can (verified on SDK 10.0.302: dotnet sln list enumerates the entry and dotnet build runs SolutionPackager through to the zip). Only dotnet sln add refuses 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 .sln keeps 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's Solutions array collapses to a single Solution object (plus a required SchemaVersion); deploy, sync, and drift drop their solution-name selector entirely, and push/generate's selector now validates against the configured solution instead of picking among several. An old multi-solution .flowline fails fast on load, naming the fix: delete the project's config and its solutions/<Name>/ folder, then flowline clone <solution> again — there's no auto-migration. Alongside this, the Solution/, Plugins/, and WebResources/ projects are now located by reading the .slnx/.sln file itself instead of assuming fixed folder names — renaming or moving any of them no longer breaks clone/push/sync/deploy, and clone now 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, and deploy now 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.

  • push no 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

  • push refuses 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 no IPlugin/CodeActivity type 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 (usually Microsoft.Xrm.Sdk.dll isn't copy-local, which hides a real plugin project's base types), or when --no-build left nothing in bin/Release to reflect. The cheap csproj-text pre-filter now defers to reflection whenever it can't be confident — a ProjectReference or a Directory.Build.props above the project means the SDK reference may never appear in the csproj itself. Standalone mode (--pluginFile) runs no discovery and is unaffected.

  • push only 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-orphans involved. Two things made that wrong. A Custom API references a plugin type as its implementation (customapi.plugintypeid points 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; --verbose says why it was left. An API with no plugintypeid at 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.

  • push now 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 .dll path above already has, so a .nupkg project pushed alongside a .dll project in the same command could delete the .dll project'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.

  • push rejects a raw assembly already registered as part of a plugin package: pushing a .dll directly (--scope assemblyonly) when Dataverse already has it registered under a PluginPackage used to silently override the package-owned assembly. It now fails with a clear message to push the .nupkg instead — 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>.xml file name), the same way WebResource/Entity/OptionSet already are, in addition to the existing id-match.

  • push no longer prints a raw stack trace for a bad [CustomApi]/[Step]/etc. attribute: every validation error PluginTypeMetadataScanner raises while reading a plugin assembly (invalid [CustomApi(UniqueName = ...)] format, a bad secondary table, and about 20 other checks) used a plain InvalidOperationException, which Program.cs's global handler doesn't recognize — it fell through to a full raw exception dump instead of a clean Error: ... line. Both places that analyze a plugin assembly (push's normal and --scope assemblyonly paths) now catch that exception and rewrap it as a FlowlineException, 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 pac itself 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 AADSTS90072 even though pac'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 under Solution/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.

  • drift no 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.LegacyPlugins unpacks to Cr07982LegacyPlugins.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.xml metadata before comparing. Nupkg PluginPackage exports, which don't strip the dot, and any DLL with no readable metadata are unaffected.

  • deploy --path no longer requires a solution file to be present: it always resolved the solution's package folder from the .slnx/.sln file even when the deploy source was an already-built zip and didn't need it, so a --path deploy 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).