dataverse-deploy
Deploy solutions, PCF controls, and web resources to Dataverse using PAC CLI
SKILL.md.claude/skills/dataverse-deploy
GitHubDataverse Deploy
Category: Operations Last Reviewed: 2026-05-16 Reviewed By: ai-procedure-quality-r1 (Phase 2b Wave 2b-A — hub #2 with 269 refs; additive-only refinement) Exemplar rationale: PCF deployment specifics change frequently with PAC CLI updates (v1.34 → v1.46 → ...). No single canonical reference holds steady. Primary Guide:
docs/guides/PCF-DEPLOYMENT-GUIDE.md
Never publish tenant-wide (owner decision D-83). Publishing every customization in the environment also publishes other people's unpublished work, so no deploy may use the all-customizations publish action, the solution-wide publish commands of
pac, or the publish-after-import flag ofpac solution import(the exact banned forms are listed intests/scripts/Publish-SolutionComponents.Tests.ps1). Import withscripts/Import-SolutionScoped.ps1(import without publish, thenPublishXmlof exactly the solution's components).PublishXmlaccepts entities, web resources, option sets, site maps, dashboards and app modules. A PCF control's new build is served only after its bundle web resources are published (the script does this). If a component type cannot be published throughPublishXml, STOP and ask the owner; never fall back to a tenant-wide publish. CI (scoped-publish-lint) fails a PR that reintroduces one.
Critical Rules
These rules are MANDATORY. See PCF-DEPLOYMENT-GUIDE.md for full details.
MUST:
- ✅ MUST use unmanaged solution unless explicitly told to use managed (ADR-022)
- ✅ MUST use Dataverse publisher
Spaarkewith prefixsprk_ - ✅ MUST rebuild fresh every deployment (
npm run build:prod, viascripts/Invoke-PcfBuildProd.ps1) - ✅ MUST stop if the PCF build failed, judged from its output:
pcf-scripts buildexits 0 when webpack fails (logs[build] Failed:/[pcf-1033]and returns), so trusting the exit code can pack and import the PREVIOUS bundle.scripts/Invoke-PcfBuildProd.ps1exits 1 on failure (found 2026-10-04: CI reported 17/18 PCFs passing when 9 had failed) - ✅ MUST copy ALL 3 files to Solution folder (bundle.js, ControlManifest.xml, styles.css)
- ✅ MUST update version in ALL 5 locations
- ✅ MUST include
.jsand.cssentries in[Content_Types].xml - ✅ MUST use
pack.ps1script (creates forward slashes in ZIP) - ✅ MUST disable/restore CPM around PAC commands
🚨 CRITICAL - Control Version is the Cache Key:
- ✅ MUST update
ControlManifest.Input.xmlversion FIRST - this is what Dataverse uses to detect updates - ✅ MUST increment the control version for EVERY deployment, not just the solution version
- ⚠️ If you only update
solution.xmlbut notControlManifest.Input.xml, Dataverse will NOT update the control
NEVER:
- ❌ NEVER use managed solution unless explicitly told - unmanaged is the default
- ❌ NEVER use or create a new publisher - always use
Spaarke(sprk_) - ❌ NEVER reuse old solution ZIPs - always pack fresh
- ❌ NEVER use
Compress-Archive- creates backslashes, breaks import - ❌ NEVER skip copying files - stale bundles cause silent failures
⚠️ AVOID (but valid fallback):
- ⚠️ AVOID
pac pcf pushfor production - rebuilds in dev mode, larger bundles - ✅ BUT use
pac pcf push --publisher-prefix sprkwhen solution imports empty (see Troubleshooting)
Deployment Workflow
Follow PCF-DEPLOYMENT-GUIDE.md for complete details.
Step 1: Build Fresh
cd src/client/pcf/{ControlName} # stay here for Steps 1-4
rm -rf out/ bin/ # a failed build must leave NO bundle behind
pwsh -File ../../../../scripts/Invoke-PcfBuildProd.ps1 -PcfPath .
# STOP on a non-zero exit. A bare `npm run build:prod` exits 0 even when webpack fails.
# Verify size (~200-400KB, NOT 8MB)
ls -la out/controls/control/bundle.js
Step 2: Update Version (5 Locations)
| # | File | Update |
|---|---|---|
| 1 | control/ControlManifest.Input.xml |
version="X.Y.Z" |
| 2 | control/{Component}.tsx |
UI version footer |
| 3 | Solution/solution.xml |
<Version>X.Y.Z</Version> |
| 4 | Solution/Controls/.../ControlManifest.xml |
version="X.Y.Z" |
| 5 | Solution/pack.ps1 |
$version = "X.Y.Z" |
Step 3: Copy Fresh Build to Solution
cp out/controls/control/bundle.js \
out/controls/control/ControlManifest.xml \
out/controls/control/styles.css \
Solution/Controls/sprk_Spaarke.Controls.{ControlName}/
Step 4: Pack and Import
# Disable CPM
mv /c/code_files/spaarke-wt-ai-rag-pipeline/Directory.Packages.props{,.disabled}
# Pack (creates fresh ZIP with forward slashes)
cd Solution && powershell -ExecutionPolicy Bypass -File pack.ps1
# Import
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath bin/{SolutionName}_vX.Y.Z.zip -SolutionUniqueName {SolutionName}
# Restore CPM
mv /c/code_files/spaarke-wt-ai-rag-pipeline/Directory.Packages.props{.disabled,}
Step 5: Verify
pac solution list | grep -i "{SolutionName}"
Hard refresh browser (Ctrl+Shift+R) and verify version footer.
Step 5.1: Verify Schema via MCP (for solutions with schema changes)
If the deployed solution included entity/attribute changes, verify the schema was applied:
USE MCP tools to confirm deployment:
mcp__dataverse__describe_table(tablename="sprk_{entity}")
CHECK:
- New columns appear in schema output
- Column types match expectations
- Lookup relationships point to correct tables
REPORT:
"✅ Post-deployment schema verification:
- sprk_{entity}: {N} columns confirmed
- New fields: {list} ✓"
This catches cases where pac solution import succeeds but schema changes are silently dropped (e.g., managed layer conflicts, missing dependencies).
⚠️ Why avoid
pac pcf pushfor production? It rebuilds in development mode, ignoring production optimizations. Bundle size increases from ~300KB to 8MB. Tree-shaking is lost. However, it is a valid fallback whenpac solution packimports an empty solution (see "Solution Imports But Is Empty" in Troubleshooting).
🚨 Dataverse Control Caching (READ THIS)
Dataverse caches PCF controls aggressively. If your deployment seems to succeed but the browser still shows old behavior, the cache wasn't busted.
Root Cause
Dataverse determines whether to update a control based on the control manifest version (ControlManifest.Input.xml), NOT just the solution version. If you:
- ✅ Update
solution.xmlversion - ❌ Forget to update
ControlManifest.Input.xmlversion
...then Dataverse sees the same control version and silently keeps the old bundle.
The Fix: Version Order Matters
Always update versions in this order:
- FIRST:
control/ControlManifest.Input.xml→version="X.Y.Z"(THIS IS THE KEY) - Then rebuild (this propagates to
out/controls/control/ControlManifest.xml) - Copy all 3 files to Solution folder
- Update
Solution/solution.xml→<Version>X.Y.Z</Version> - Update
Solution/pack.ps1→$version = "X.Y.Z"
Nuclear Option: Delete and Reimport
If you've deployed but the control is still cached:
# Delete the solution completely from Dataverse
pac solution delete --solution-name {SolutionName}
# Reimport fresh
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath bin/{SolutionName}_vX.Y.Z.zip -SolutionUniqueName {SolutionName}
# Verify
pac solution list | grep -i "{SolutionName}"
Then hard refresh browser (Ctrl+Shift+R).
Symptoms of Caching Issue
| Symptom | Likely Cause |
|---|---|
pac solution import succeeds but UI unchanged |
Control version not incremented |
pac solution list shows new version but old behavior |
Control manifest version mismatch |
| Hard refresh doesn't help | Need delete + reimport |
| Same control works in one browser but not another | Browser cache - try incognito |
Decision Tree: Which Workflow?
Is the PCF embedded in a Custom Page?
├── YES → See "Custom Page Deployment" section in guide
└── NO → Use "Deployment Workflow" above (build → copy → pack.ps1 → import)
Primary Guide: docs/guides/PCF-DEPLOYMENT-GUIDE.md - Complete workflow with version management and troubleshooting.
Purpose
Deploy Dataverse components using PAC CLI following the PCF-DEPLOYMENT-GUIDE.md. This skill handles authentication, Central Package Management conflicts, and solution import issues.
Best Practices
| Practice | Implementation |
|---|---|
| Always use unmanaged | Never export/pack as managed unless user explicitly requests |
| Always use Spaarke publisher | Never create a new publisher - use Spaarke (sprk_) |
| Always build fresh | Run scripts/Invoke-PcfBuildProd.ps1 (npm run build:prod + a failure check, since pcf-scripts exits 0 on a failed build); never reuse old artifacts |
| Always use pack.ps1 | Never use Compress-Archive (creates backslashes) |
| Version footer | Every PCF MUST display vX.Y.Z • Built YYYY-MM-DD in the UI |
| Version bumping | Increment version in ALL 5 locations |
| Verify deployment | ALWAYS run pac solution list after import to confirm version |
| Use React 16 APIs | Dataverse provides React 16.14.0 - see ADR-022 |
Key Guidance
- Prefer pack.ps1 workflow for production - build → copy files → pack.ps1 → import
- Use
pac pcf pushas fallback when solution imports empty (Customizations.xml issue) - Version Locations: Update ALL 5: (1) ControlManifest.Input.xml, (2) UI footer, (3) Solution.xml, (4) extracted ControlManifest.xml, (5) pack.ps1
- Full Guide: See
docs/guides/PCF-DEPLOYMENT-GUIDE.mdfor complete workflow
Prerequisites Check
Before ANY deployment operation:
# 1. Check PAC CLI is installed
pac --version
# 2. Check authentication status
pac auth list
# 3. Verify active connection points to correct environment
# Look for: Active = *, Environment URL matches expected target
Expected Output
Index Active Kind Name Cloud Type Environment Environment Url
[1] UNIVERSAL dev Public User HIPC DEV 2 https://hipc-dev-2.crm.dynamics.com/
[2] * UNIVERSAL prod Public User SPAARKE DEV 1 https://spaarkedev1.crm.dynamics.com/
If No Active Auth
# Create new authentication
pac auth create --environment "https://YOUR-ENV.crm.dynamics.com"
# Or select existing profile
pac auth select --index 1
Additional Scenarios
Scenario 1: PCF Control Deployment
Use the "Deployment Workflow" section above. Follow PCF-DEPLOYMENT-GUIDE.md for the complete workflow:
- Build fresh with
scripts/Invoke-PcfBuildProd.ps1(runsnpm run build:prod; stop on a non-zero exit) - Update version in ALL 5 locations
- Copy ALL 3 files to Solution folder
- Pack with
pack.ps1(NOTCompress-Archive) - Import with
pac solution import - Verify with
pac solution list
Scenario 1b: PCF Control with Platform Libraries (Large Controls)
Use when: PCF bundle exceeds 5MB due to bundled React/Fluent UI.
If your bundle is > 1MB, you're likely bundling React and Fluent UI. Use platform libraries so Dataverse provides these at runtime.
Check Bundle Size
ls -la out/controls/*/bundle.js
# Should be 300-500KB with platform libraries, 5MB+ without
Fix ControlManifest.Input.xml
Add <platform-library> elements:
<resources>
<code path="index.ts" order="1" />
<!-- Host-provided: DO NOT bundle React/Fluent -->
<platform-library name="React" version="16.14.0" />
<platform-library name="Fluent" version="9.46.2" />
</resources>
Create featureconfig.json (CRITICAL)
pcf-scripts requires a feature flag to externalize ReactDOM:
{
"pcfReactPlatformLibraries": "on"
}
Without this file, React is externalized but ReactDOM is still bundled, causing React version mismatch errors at runtime.
Enable Custom Webpack for Icon Tree-Shaking (CRITICAL for large bundles)
If your bundle is still large (>500KB) after adding platform libraries, @fluentui/react-icons is likely not tree-shaking. The full icon library is ~6.8MB.
Solution: Enable custom webpack with sideEffects: false for icons:
- Update featureconfig.json (add
pcfAllowCustomWebpack):
{
"pcfReactPlatformLibraries": "on",
"pcfAllowCustomWebpack": "on"
}
- Create webpack.config.js in control root:
// Custom webpack configuration for PCF
// Enables tree-shaking for @fluentui/react-icons
module.exports = {
optimization: {
usedExports: true,
sideEffects: true,
innerGraph: true,
providedExports: true
},
module: {
rules: [
{
// Mark @fluentui/react-icons as side-effect-free
test: /[\\/]node_modules[\\/]@fluentui[\\/]react-icons[\\/]/,
sideEffects: false
}
]
}
};
Result: Bundle size typically drops from 5-9MB to 200-400KB.
⚠️ NOTE:
pac pcf pushrebuilds in development mode, ignoring these optimizations. Use the pack.ps1 workflow instead to preserve production build.
Fix package.json
Move React/Fluent to devDependencies (type-checking only):
{
"devDependencies": {
"@types/react": "^16.14.0",
"@types/react-dom": "^16.9.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"@fluentui/react-components": "^9.46.0"
}
}
Note: Use React 16 types to match the platform runtime. See ADR-022.
Remove from dependencies: any react, react-dom, or @fluentui/react-* packages (keep in devDependencies only).
Full details: See
docs/guides/PCF-DEPLOYMENT-GUIDE.md
Scenario 1c: PCF Control in Custom Page (COMPLEX)
Use when: PCF control is embedded in a Canvas App Custom Page.
⚠️ WARNING: This is the most complex deployment scenario. When a PCF is used inside a Custom Page, THREE version locations must stay synchronized.
See detailed guide: docs/guides/PCF-DEPLOYMENT-GUIDE.md (Custom Page section)
Quick Summary
| Location | Purpose | Updated By |
|---|---|---|
| Dataverse Registry | Master version | pac pcf push or Solution Import |
| Solution Controls Folder | Exported artifact | Manual copy |
| Canvas App Embedded | Runtime bundle | Manual copy + pac canvas pack |
The Problem
When you open a Custom Page in Power Apps Studio, it may downgrade your PCF if the Dataverse Registry has an older version.
Key Rules
- ALWAYS update Dataverse Registry FIRST before opening Power Apps Studio
- ALWAYS copy bundle to BOTH locations (Controls folder AND Canvas App embedded)
- NEVER open Power Apps Studio until all three locations are synchronized
Scenario 1d: PCF Production Release (Full Solution Workflow)
Use when: Deploying a PCF update with proper version tracking for production.
⚠️ CRITICAL:
pac pcf pushdoes NOT update your named solution's version. Use this workflow for production releases.
See detailed guide: docs/guides/PCF-DEPLOYMENT-GUIDE.md
Why This Workflow?
| Method | Updates Dataverse Control | Updates Named Solution Version | Best For |
|---|---|---|---|
pac pcf push |
✅ Yes | ❌ No (creates temp solution) | Dev testing |
| Solution Import | ✅ Yes | ✅ Yes | Production releases |
Quick Steps
# 1. Build (exits 1 if the build failed; a bare `npm run build:prod` exits 0 even then)
cd src/client/pcf/{ControlName}
pwsh -File ../../../../scripts/Invoke-PcfBuildProd.ps1 -PcfPath .
# 2. Update version in every location the pcf-deploy skill's version table lists (manual)
# 3. Copy bundle to solution folder
cp out/controls/control/bundle.js \
infrastructure/dataverse/solutions/{Solution}/Controls/{namespace}.{ControlName}/
# 4. Pack and import
pac solution pack --zipfile Solution_vX.Y.Z.zip --folder {Solution}
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath Solution_vX.Y.Z.zip -SolutionUniqueName {SolutionName}
# 5. Verify
pac solution list | grep -i "{SolutionName}"
Scenario 2: Solution Export
Use when: Backing up or extracting a solution for modification.
# List available solutions
pac solution list
# Export unmanaged solution (ALWAYS use unmanaged)
pac solution export --name "{SolutionName}" --path "./{SolutionName}.zip" --managed false
⚠️ NEVER export as managed unless the user explicitly requests it. Managed solutions have caused issues in past projects.
Scenario 3: Solution Import
Use when: Deploying a solution package to an environment.
# Import, then publish only the imported components (the script already passes --force-overwrite)
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath "./{SolutionName}.zip" -SolutionUniqueName {SolutionName}
Post-Import Verification
# Check solution was imported
pac solution list | grep -i "{SolutionName}"
# The script reads web resources and app modules back and fails if any is still unpublished.
# To see what would be published without importing:
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -SolutionUniqueName {SolutionName} -PlanOnly
Scenario 4: Web Resource Deployment
Use when: Deploying JavaScript, CSS, or image files.
Include web resources in a solution and use Scenario 3.
# Export solution containing web resources
pac solution export --name "SpaarkeCore" --path "./SpaarkeCore.zip" --managed false
# Extract, modify, repack, import
unzip SpaarkeCore.zip -d SpaarkeCore_extracted
# ... modify files in WebResources folder ...
pac solution pack --zipfile SpaarkeCore_modified.zip --folder SpaarkeCore_extracted
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath SpaarkeCore_modified.zip -SolutionUniqueName SpaarkeCore
Scenario 5: Publish Customizations
Use when: a change was made outside a solution import (a Web API PATCH of a web resource, view, form or entity). Publish only that component: POST /api/data/v9.2/PublishXml with {ParameterXml} built by New-PublishParameterXml in scripts/lib/Publish-SolutionComponents.ps1 (for example -WebResources <id> or -Entities sprk_event). Entity publishes are entity-wide: PublishXml has no per-view or per-form element, so publishing an entity also publishes every pending view and form of that entity. The script STOPS before any import or publish when that would also publish someone else's pending change (D-103); see "Pending collateral" below. Never a tenant-wide publish.
Workflows, resume, read-back and the probe method (task 130)
A solution that contains a workflow or cloud flow (component type 29). The scoped import refuses it by default: the RetrieveUnpublished probe answers "record does not exist" (error 0x80040217) for workflows, so the function is bound to the table and a draft layer cannot be ruled out, and a workflow is activated by the import, not by PublishXml. When the solution really contains one, run the import with -AllowWorkflows:
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath <zip> -SolutionUniqueName <name> -AllowWorkflows
This adds --activate-plugins to the pac import (pac's help: "Activate plug-ins and workflows on the solution"), publishes the rest as usual, and afterwards reads workflows(id).statecode for every workflow in the solution; it fails unless each is 1 (Activated).
If a publish request fails part-way. The error prints the resume command; run it, it does not re-import:
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -SolutionUniqueName <name> -PublishOnly
Requests go out in this order so nothing goes live before what it uses: option sets and web resources, entities, the application ribbon, site maps and dashboards, app modules last. Components a caller published outside the solution are listed in the failure and carried by the resume command (-ExtraWebResources / -ExtraEntities).
Read-back and its known limit. After every request the script compares web resources, app modules, app settings, site maps, dashboards and (for each published entity) system forms, saved queries and charts with their unpublished copies. Option sets and the application ribbon have no read-back surface: check them by effect.
Probe method (why a type is on the no-publish list). Call <set>(<fake id>)/Microsoft.Dynamics.CRM.RetrieveUnpublished(). Only error code 0x80060888 ("Resource not found for the segment") proves the table is not bound to the unpublished-layer function. Error 0x80040217 ("... Does Not Exist") means the function is bound and only the record is missing, so the table has a draft layer.
Pending collateral stops the publish (D-103)
Publishing an entity also publishes every pending (unpublished) view, form and chart of that entity, and publishing an app setting publishes its whole parent app. If that would publish someone else's pending change, scripts/Import-SolutionScoped.ps1 STOPS before pac solution import and before any PublishXml, lists each entity or app and each pending component, and prints the exact re-run command with the opt-in flag -AllowPendingCollateral. Do not add the flag unless the owner has said publishing those items is intended. With the flag the script warns, lists and continues. -PublishOnly applies the same rule, and the resume command carries the flag only if the original run had it. The solution's own items (including every subcomponent of an entity included with all its subcomponents) and parent apps that are part of the solution are not collateral.
Conventions
Publisher
Always use Spaarke publisher with prefix sprk_ - NEVER create a new publisher.
| Setting | Value |
|---|---|
| Unique Name | Spaarke |
| Prefix | sprk |
| Option Value Prefix | 65949 |
Naming examples:
- PCF controls:
sprk_Spaarke.Controls.{ControlName} - Web resources:
sprk_FileName.js - Entities:
sprk_entityname - Fields:
sprk_fieldname
Working Directories
| Component Type | Directory |
|---|---|
| PCF Controls | src/client/pcf/{ControlName} |
| Web Resources JS | src/client/webresources/js/ |
| Ribbon XML | infrastructure/dataverse/ribbon/ |
| Solutions | infrastructure/dataverse/solutions/ |
Error Handling
| Error | Cause | Solution |
|---|---|---|
No active authentication profile |
Not logged in | Run pac auth create --environment "URL" |
NU1008: Projects that use central package version management |
Directory.Packages.props conflict | Disable CPM before PAC commands |
Unable to remove directory "obj\Debug\Metadata" |
File lock during cleanup | Harmless - import the packed solution directly |
Solution not found |
Wrong solution name | Run pac solution list to find exact name |
Publisher not found |
Wrong publisher prefix | Use --publisher-prefix sprk |
Import failed: Dependency not found |
Missing dependent solution | Import dependencies first |
File exceeds 5MB limit |
React/Fluent bundled | Use platform libraries (Scenario 1b) |
| PCF version regresses in Custom Page | Registry has older version | Update registry FIRST (Scenario 1c) |
PowerAppsToolsTemp solutions appear |
Created by pac pcf push |
Delete after deployment if needed |
Cannot create property '_updatedFibers' |
Using React 18 APIs with React 16 runtime | Use ReactDOM.render(), not createRoot() - see ADR-022 |
createRoot is not a function |
Importing from react-dom/client |
Import from react-dom instead |
| Solution zip not created | pack.ps1 failed | Check pack.ps1 script exists and run manually |
Orphaned component blocking deployment |
Namespace changed or old controls exist | Delete orphaned controls via Web API (see below) |
CustomControls Source File styles.css does not exist |
styles.css not copied to solution folder | Copy ALL 3 files to Solution folder |
| Solution import succeeds but UI shows old behavior | Control manifest version not updated | Update ControlManifest.Input.xml version FIRST, rebuild, then deploy |
| Deployment seems stuck on old version | Dataverse control cache | Delete solution with pac solution delete, then reimport fresh |
| Solution imports but shows 0 components | Empty Customizations.xml |
Use pac pcf push fallback (see below) |
🚨 Solution Imports But Is Empty (0 Components)
Symptoms:
pac solution importsucceedspac solution listshows solution with correct version- But solution in portal shows "All: 0" - no components
- PCF control doesn't appear or doesn't update
Root Cause:
The Customizations.xml file in your Solution folder has empty component sections:
<CustomControls />
<WebResources />
When you run pac solution pack, it packs exactly what's in Customizations.xml. If the component references are missing, you get an empty solution that imports but does nothing.
Solution - Use pac pcf push as Fallback:
cd src/client/pcf/{ControlName}
# pac pcf push generates proper Customizations.xml automatically
pac pcf push --publisher-prefix sprk
# If you get file lock error during cleanup, ignore it - solution is already packed
# Import the generated solution
pwsh scripts/Import-SolutionScoped.ps1 -EnvironmentUrl <url> -ZipPath "out/PowerAppsTools_sprk/bin/Debug/PowerAppsTools_sprk.zip" -SolutionUniqueName PowerAppsTools_sprk
# Verify
pac solution list | grep -i "{ControlName}"
Why This Works:
pac pcf push generates a proper Customizations.xml with component references:
<CustomControls>
<CustomControl>
<Name>sprk_Spaarke.Controls.UniversalDocumentUpload</Name>
<FileName>/Controls/sprk_Spaarke.Controls.UniversalDocumentUpload/ControlManifest.xml</FileName>
</CustomControl>
</CustomControls>
Prevention:
After using pac pcf push successfully:
- Examine the generated
Customizations.xmland update your Solution folder's version to match the structure - IMPORTANT:
pac pcf pushbypasses the Solution folder entirely - it builds from source and deploys directly to Dataverse. You must still copy the fresh bundle.js and ControlManifest.xml to your Solution folder to keep it in sync for futurepac solution packdeployments.
# After pac pcf push, sync Solution folder:
cp out/controls/control/bundle.js \
out/controls/control/ControlManifest.xml \
Solution/src/WebResources/sprk_Spaarke.Controls.{ControlName}/
This prevents the Solution folder from becoming stale and ensures future solution imports work correctly.
Orphaned Control Cleanup
When namespace changes (e.g., Spaarke.PCF → Spaarke.Controls) or old deployments exist, orphaned controls can block new deployments.
Symptoms:
- Deployment fails with duplicate component errors
- Multiple versions of same control in solution list
- "Component with same name already exists" errors
Solution - Delete via Web API:
# 1. Find the orphaned control's ID
# Use Dataverse Web API or Advanced Find
# 2. Delete using PAC CLI or Web API
pac org fetch --filter "customcontrolid eq 'GUID-HERE'"
# 3. Or use Power Platform Admin Center:
# - Go to Environments → Your Environment → Settings → Solutions
# - Find and delete orphaned components
Prevention:
- Always use consistent namespace (e.g.,
Spaarke.Controls) - Delete old solutions before changing namespace
- Use
pac solution deleteto cleanly remove old solutions
Quick Reference Commands
# Authentication
pac auth list # Show all auth profiles
pac auth create --environment "URL" # Create new profile
pac auth select --index N # Switch profile
# Solutions
pac solution list # List all solutions
pac solution export --name X --path Y # Export solution (add --managed false)
pac solution import --path Y # Import solution
# no tenant-wide publish: use scripts/Import-SolutionScoped.ps1
# PCF Controls - Use pack.ps1 workflow (see Deployment Workflow above)
pwsh -File ../../../../scripts/Invoke-PcfBuildProd.ps1 -PcfPath . # in src/client/pcf/X: build control (fails on a failed build)
powershell -File Solution/pack.ps1 # Pack solution (NOT Compress-Archive)
pac solution import --path bin/X.zip # Import solution
# Troubleshooting
pac org who # Show current org info
pac solution check --path Y # Validate solution before import
Related Skills
ribbon-edit- Ribbon customizations use solution export/import workflowspaarke-conventions- Naming conventions for all Dataverse componentsadr-aware- ADR-006 governs PCF control patterns, ADR-022 governs React versionci-cd- CI/CD pipeline status and automated deployment workflows
CI/CD Integration
No Plugin Deployment (ADR-002, 2026-09-25)
Spaarke ships no Dataverse plugins, so there is no plugin deployment — manual or automated. (A deploy-staging.yml / deploy-plugins job and a Spaarke.Plugins.dll were previously documented here; neither ever existed.) Do not run pac plugin push or register plugin steps against Spaarke environments. The only permitted Dataverse step registrations are no-code service-endpoint/webhook steps used as async change signals (ADR-002 WP-5) — see docs/architecture/DATAVERSE-WRITE-PATH-ARCHITECTURE.md.
When to Use This Skill
| Scenario | Use |
|---|---|
| PCF control iterative development | This skill - Quick Dev Deploy |
| Production solution release | This skill - Scenario 1d |
| Custom Page updates | This skill - Scenario 1c |
| Emergency hotfix | This skill |
Related ADRs
| ADR | Relevance |
|---|---|
| ADR-006 | PCF over legacy webresources |
| ADR-022 | React 16 compatibility (CRITICAL) |
| ADR-021 | Fluent UI v9 design system |
Resources
| Resource | Purpose |
|---|---|
docs/guides/PCF-DEPLOYMENT-GUIDE.md |
Primary guide - Consolidated PCF deployment workflow |
Operator Notes
🚨 CRITICAL: Control Manifest Version is the Cache Key
When deploying PCF updates, the #1 cause of "deployment succeeded but nothing changed" is forgetting to update the control manifest version. Follow this exact order:
- FIRST: Update
control/ControlManifest.Input.xmlversion attribute - THEN: Rebuild with
scripts/Invoke-PcfBuildProd.ps1 -PcfPath src/client/pcf/{ControlName}(runsnpm run build:prod; it exits 1 when the build failed, which a barenpm run build:proddoes not) - THEN: Copy ALL 3 files to Solution folder
- THEN: Update
solution.xmlandpack.ps1versions - THEN: Pack and import
If the user reports the control isn't updating after deployment:
- Check if
ControlManifest.Input.xmlversion was incremented - If not, increment it, rebuild, and redeploy
- If still stuck, use the nuclear option:
pac solution deletethen reimport
Never assume that updating solution.xml alone will bust the cache. The control manifest version is what Dataverse checks.
🚨 CRITICAL: Solution Imports But Shows 0 Components
If pac solution pack + pac solution import succeeds but the solution is empty in the portal:
- Root cause:
Customizations.xmlhas empty<CustomControls />section - Fix: Use
pac pcf push --publisher-prefix sprkas fallback - Why it works:
pac pcf pushauto-generates proper component references - File lock error during cleanup is harmless - solution is already packed, import it directly
- CRITICAL: After
pac pcf push, sync the Solution folder -pac pcf pushbypasses it entirely, leaving it stale:cp out/controls/control/bundle.js out/controls/control/ControlManifest.xml \ Solution/src/WebResources/sprk_Spaarke.Controls.{ControlName}/ - Prevention: Copy the generated
Customizations.xmlstructure to your Solution folder for futurepac solution packdeployments
Failure Modes & Recovery
| Failure | Cause | Prevention / Recovery |
|---|---|---|
pac pcf push "succeeds" but Dataverse shows old control version |
PAC bypasses solution.xml versioning — Dataverse cache hits the old version. See the four 🚨 CRITICAL callouts in this skill |
Always bump ControlManifest.Input.xml version= BEFORE pac pcf push. Sync the Solution folder after push (Critical Rule #5) so subsequent pac solution pack deployments aren't stale. |
| Empty solution import succeeds but no controls deploy | Solution ZIP missing the control root in Solution/src/WebResources/<sprk_...>/, OR customizations.xml doesn't reference the control |
Verify ZIP contents BEFORE import: unzip -l <solution>.zip should show the control bundle.js + ControlManifest.xml at the expected path. |
| Solution import fails with "missing dependency" | Required entity/option set not yet in the target environment | Run pac solution check --solution-zip <path> BEFORE import. Resolve dependencies upstream first. |
| Bundle deployed but PCF renders blank | Bundle is dev-mode (not tree-shaken) — exceeded Dataverse runtime size limit silently. See pcf-deploy AP-1 for canonical fix |
Always use npm run build:prod (NOT npm run build). Bundle size sanity check: out/controls/<name>/bundle.js should be < 1 MB for typical controls. |
| Web resource import succeeds but resource doesn't appear in form | Cache or publish step missing | Publish the web resource only: re-run scripts/Import-SolutionScoped.ps1, or POST PublishXml with <webresources> for it. Never the maker portal's all-customizations publish button. Refresh browser. |