Plugin check: Run
node "${CLAUDE_PLUGIN_ROOT}/scripts/check-version.js"— if it outputs a message, show it to the user before proceeding.
deploy-pipeline
Triggers a Power Platform Pipeline deployment run. Reads the existing pipeline configuration from docs/alm/last-pipeline.json, selects a target stage, validates the solution package, and deploys it to the target environment.
Prerequisite: Run
/power-pages:setup-pipelinefirst to create the pipeline configuration.
Refer to
${CLAUDE_PLUGIN_ROOT}/references/cicd-pipeline-patterns.mdfor all HAR-confirmed API patterns used in this skill.
Prerequisites
Important: The source (dev) environment must have a Power Platform Pipelines host environment configured. This is set in Power Platform Admin Center (Environments → select env → Pipelines) or via the tenant-level
DefaultCustomPipelinesHostEnvForTenantsetting. Without this configuration,pac pipeline deploywill fail. Thesetup-pipelineskill creates the pipeline definition in the host; this admin step connects the dev environment to that host.
docs/alm/last-pipeline.jsonexists (created bysetup-pipeline).solution-manifest.jsonexists- Azure CLI logged in (
az account showsucceeds) - PAC CLI logged in (
pac env whosucceeds)
Phases
Phase 0 — ALM plan gate
plan-almis the front door. When the user expresses an ALM intent (promote / ship / deploy / move to staging / push to prod / release this version), the orchestrator (/power-pages:plan-alm) should run first. Direct invocation ofdeploy-pipelinebypasses the orchestrator's pre-plan completeness check, env-var resolution per stage, activation steps, and validation runs. This gate makes that bypass explicit.
Skip rule. If this skill was invoked as part of an active plan-alm orchestration, skip Phase 0 entirely and proceed to Phase 1. The gate helper exposes this via its inExecution block — pass through silently to Phase 1 when:
inExecution.status === "active"
The helper computes this from docs/.alm-plan-data.json — PLAN_STATUS === "In Execution" AND LAST_INVOCATION_AT within the last 60 minutes. check-alm-plan.js refreshes LAST_INVOCATION_AT automatically on every invocation that finds the plan in execution, so each in-chain skill keeps the chain alive for the next one — even multi-hour deploys (deploy-pipeline alone can take 60 min per stage) survive the window without the chain incorrectly de-classifying. Stalled chains (no heartbeat for > 60 min) reclassify as stale-heartbeat and Phase 0 gates fire normally so an abandoned plan doesn't silently bypass user confirmation.
When inExecution.status is anything other than "active" ("not-running", "stale-heartbeat", "no-plan"), run the Phase 0 gate flow below. Branch on the remaining helper fields:
Step 1 — Run the gate helper.
node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" \
--projectRoot "." \
--envUrl "{devEnvUrl}" \
--token "{token}" \
--solutionId "{solutionId from .solution-manifest.json, if available}"
The helper returns JSON with { exists, stale, staleness: { reason, detail }, generatedAt, planStatus, ... }. The freshness check requires env credentials + solutionId; without those the helper does an existence-only check.
Step 2 — Branch on the result.
| Result | Behavior |
|---|---|
deferred: true |
The user has explicitly deferred ALM for this project (.alm-deferred marker present). Pass through silently to Phase 1 — do not nag. |
exists: false |
The user hasn't run plan-alm yet. See Step 3. |
exists: true, stale: false |
Plan is current. Pass through silently to Phase 1. |
exists: true, stale: true (reason: solution-modified) |
The solution changed after the plan was generated. See Step 4. |
Step 3 — No plan. Tell the user:
"No ALM plan exists for this project.
/power-pages:plan-almbuilds one — it detects the project state, asks about your promotion strategy, and orchestrates this skill in the right order alongside setup-solution / setup-pipeline / activate-site / test-site. Want me to run plan-alm now?"
🚦 Gate (intent · deploy-pipeline:0.no-plan): Fail-closed entry gate when
check-alm-plan.jsreturnsexists:false. Helper-script-backed.
AskUserQuestion:
| Question | Header | Options |
|---|---|---|
Run /power-pages:plan-alm first? |
ALM plan gate | Yes — run /power-pages:plan-alm now (Recommended), Continue without a plan (advanced — I just want to deploy), Cancel |
- Yes (Recommended) → invoke
/power-pages:plan-alm. plan-alm's Phase 7 dispatches back into this skill at the appropriate stage. - Continue without a plan → set
BYPASSED_PLAN_GATE = trueand proceed to Phase 1. The deploy will still work, but env-var per-stage values, activation, and post-deploy validation aren't orchestrated. - Cancel → exit cleanly.
Step 4 — Stale plan. Tell the user:
"ALM plan exists from
{generatedAt}but the source solution has been modified since (at{solution.modifiedon}). The plan's component count, size analysis, and split decisions may be outdated. Re-runningplan-almwill refresh the analysis."
🚦 Gate (intent · deploy-pipeline:0.stale-plan): Fail-closed entry gate when
check-alm-plan.jsreturnsstale:true. Helper-script-backed.
AskUserQuestion:
| Question | Header | Options |
|---|---|---|
| Refresh the plan first? | ALM plan freshness | Refresh — re-run /power-pages:plan-alm (Recommended), Continue with the existing plan, Cancel |
- Refresh (Recommended) → invoke
/power-pages:plan-alm. After completion, re-run the Phase 0 helper once to confirm freshness; if still stale, surface the detail and proceed to Phase 1 anyway (don't infinite-loop). - Continue → set
STALE_PLAN_ACK = trueand proceed to Phase 1. - Cancel → exit cleanly.
Relationship to Phase 3.5 (pre-deploy completeness check). Phase 3.5 (later in this skill) catches solution gaps right before deploy. Phase 0 catches the bigger miss: the user who never ran the orchestrator at all and is about to push a half-baked deploy through. The two are complementary.
Phase 1 — Verify Prerequisites
Create all tasks upfront at the start of this phase.
Tasks to create:
- "Verify prerequisites"
- "Select target stage"
- "Resolve pipeline info"
- "Validate package" — in
MULTI_RUN_MODEthis becomes a single parallel batch (Phase 3.6) covering all N non-skipped solutions; in single-solution / legacy v2 mode it runs per-iteration inline in Phase 4 - "Configure deployment settings"
- "Deploy and monitor"
- "Write deployment record"
Steps:
Run
verify-alm-prerequisites.jsto confirm PAC CLI auth, acquire a token, and verify API access:node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --require-manifestCapture output as JSON; extract
.envUrl(store asdevEnvUrl) and.token(store asDEV_TOKEN). If the script exits non-zero, stop and surface the error — it will indicate whetheraz login,pac auth, or WhoAmI failed.Run
detect-project-context.jsto read project config and solution manifest:node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/detect-project-context.js"Capture output as JSON; extract
.solutionManifest(store assolutionManifest),.siteName(store assiteName), and.websiteRecordId. IfsolutionManifestis null, continue — the manifest is not strictly required at this step (solution info will come fromdocs/alm/last-pipeline.json).Locate
docs/alm/last-pipeline.json— if not found, stop and advise running/power-pages:setup-pipelinefirst.Manifest version check:
- If
schemaVersion === 3, setMULTI_RUN_MODE = trueand storedeploymentOrder[]asDEPLOYMENT_ORDER. There is a single pipeline with a single set of stages; multi-solution is expressed via N stage runs against the same stage, one per solution inorder. This is the current recommended layout. - If
schemaVersion === 2(legacy), setMULTI_PIPELINE_MODE = trueand storepipelines[]asPIPELINES_LIST. The skill falls back to the older "loop over N separatedeploymentpipelinesrecords" behavior. Advise the user to re-runsetup-pipelineto migrate to v3. - Otherwise read
pipelineId,pipelineName,hostEnvUrl,sourceDeploymentEnvironmentId,solutionName,stages[](single-solution mode — existing behavior).
In
MULTI_RUN_MODE, resolvesolutionName+solutionIdper iteration ofDEPLOYMENT_ORDER. Entries wherestatus === "SkippedEmpty"(typically the{Prefix}_Futurebuffer) are short-circuited — no stage run is created for them. The singlepipelineId/hostEnvUrl/sourceDeploymentEnvironmentIdapply to every run.In
MULTI_PIPELINE_MODE(legacy v2), resolvesolutionNameper pipeline in the loop (not globally). All pipelines share the samehostEnvUrlandsourceDeploymentEnvironmentId.- If
Acquire host environment token:
az account get-access-token --resource "{hostEnvOrigin}" --query accessToken -o tsv 2>/dev/nullWhere
hostEnvOrigin= scheme + host ofhostEnvUrl. Store asHOST_TOKEN. If acquisition fails, stop with instructions to check Azure CLI auth.If
solutionManifestis available, readsolutionManifest.solution.solutionIdandsolutionManifest.solution.uniqueNamefrom the detected context. Otherwise, usesolutionNamefromdocs/alm/last-pipeline.json.Report: "Pipeline:
{pipelineName}. Solution:{solutionName}. Available stages:{stage names}."
Phase 1.5 — Ground in current Pipelines deployment documentation
Reference:
${CLAUDE_PLUGIN_ROOT}/references/alm-docs-grounding.md
Cap this step at ~30 seconds. If MCP search / fetch errors out, log a one-line note and continue — this skill must remain runnable offline.
- Run
microsoft_docs_searchwith the query:Power Platform Pipelines stage run validation ValidatePackageAsync DeployPackageAsync approval. - Fetch
https://learn.microsoft.com/en-us/power-platform/alm/pipelines(and at most one sister page on stage runs, validation, or approval gates) in parallel viamicrosoft_docs_fetch. - Extract a one-paragraph summary of what Microsoft Learn currently says about stage-run lifecycle, validation outcomes, approval-gate workflow, and
deploymentsettingsjsonoverrides. Compare against${CLAUDE_PLUGIN_ROOT}/references/cicd-pipeline-patterns.mdand flag any divergence (new status codes, changedstagerunstatusterminal values, new approval-gate API). - Use the summary to inform Phase 2+ decisions. Do not silently change skill behavior — surface any divergence to the user as a soft warning before Phase 4 (Create Stage Run + Validate Package).
Phase 2 — Select Target Stage
If the user passed a stage name or environment label as an argument (e.g., staging), match it against stages in docs/alm/last-pipeline.json and skip this question.
🚦 Gate (plan · deploy-pipeline:2.stage): Pick target stage — Staging / Production / etc. Wrong stage selection here is the biggest single failure mode of this skill.
Otherwise, ask via AskUserQuestion:
"Which environment do you want to deploy to? {numbered list of stages from docs/alm/last-pipeline.json, e.g.:
- Deploy to Staging → {stagingEnvUrl}
- Deploy to Production → {prodEnvUrl}}"
Store selected stage as SELECTED_STAGE (with stageId, name, targetDeploymentEnvironmentId, targetEnvironmentUrl).
Design rationale — the deploy loop is serial by design. When
DEPLOYMENT_ORDERhas N entries (e.g.Core → WebAssets → Futurefor a 2-solution split with a future buffer), the loop runs one solution at a time, inorderascending, halt-on-first-failure. This is intentional. Four constraints stack up and make parallelDeployPackageAsynccalls actively harmful, not just non-beneficial:
- Dataverse import lock at the target env.
ImportSolutionAsynctakes an env-level lock — only one solution can actively import at a time per environment. Even if the skill fired NDeployPackageAsynccalls in parallel, the host would queue them and run them serially anyway. The wall-clock win for parallel deploys is effectively zero.- Inter-split dependencies in 3 of 4 split strategies. Change Frequency (
Foundation → Integration → Config → Content), Schema (Domain_1..N → Site), and Layer (Core → WebAssets, where WebAssets ppc rows reference thepowerpagesiterecord in Core) all encode strict ordering. Re-ordering breaks the import — a flow that references a table not yet in the target fails withMissingDependency.- Per-iteration consent gates. Phase 6.0 (
deploy-pipeline:6.0.final-consent) fires before EVERYDeployPackageAsync. The Phase 5 env-var prompt fires per iteration too. Parallel execution would require either batching the gates (explicitly forbidden — see the per-iteration callout below) or running concurrentAskUserQuestions, neither of which the harness supports.- Clean failure handling. Serial + halt-on-first-failure means
docs/alm/last-deploy.jsonrecords per-solutionstatuscleanly. On retry the loop iterates from the start; Dataverse's same-version idempotency turns already-landed solutions into no-ops.Do not "optimize" this loop by wrapping iterations in
Promise.all/Promise.allSettled/await Promise.race. The validation phase (Phase 3.6) IS parallelized —ValidatePackageAsyncdoes NOT take the import lock — but the deploy phase is intentionally serial. If a future Dataverse release removes the env-level import lock, revisit this rationale; until then it is load-bearing, not an oversight.
In MULTI_RUN_MODE (v3 — recommended): The selected stage is looked up once from the single stages[] array. The skill then loops over DEPLOYMENT_ORDER in order, creating one stage run per solution against the same stageId:
- Phase 3.6 runs first (once, before the loop) — fans out
create-stage-run+ValidatePackageAsync+poll-validation-statusfor every non-skipped solution in parallel. Halts the entire deploy if any solution fails validation. Stores the per-solutionstageRunIdinVALIDATED_STAGE_RUNSso the serial deploy loop can reuse them. - For each entry in
DEPLOYMENT_ORDERwherestatus !== "SkippedEmpty": resolve itssolutionUniqueName+solutionId, retrieve itsstageRunIdfromVALIDATED_STAGE_RUNS[solutionUniqueName], setARTIFACT_SOLUTION_NAME/ARTIFACT_SOLUTION_ID/STAGE_RUN_ID, then run Phases 4.4 (fetch deployment notes) → 5 (configure) → 6.0 (consent gate fires every iteration) → 6.1 (deploy) → 6.2 (poll) against the same pipeline. Phase 4.1–4.3 (create stage run + validate + poll-validation) are skipped — Phase 3.6 already did the work in parallel. - If any iteration fails (deployment), halt the loop and report which solution failed and which had already landed.
- Write one
docs/alm/last-deploy.jsonat the end summarizing all runs for the selected stage. Record per-solutionstatus(Succeeded/Failed/NotAttempted/SkippedEmpty) plus the sharedpipelineId.
⚠ Per-iteration gate firing — non-negotiable. Inside the loop, the full Phase 3 → 3.5 → 4 → 5 → 6.0 → 6.1 → 6.2 → 7 sequence runs for each solution. Do NOT batch validation across solutions, do NOT batch the Phase 6.0 consent gate, and do NOT treat any upstream answer (Phase 2 stage selection,
--stageargument, the previous iteration's "Deploy now") as covering subsequent iterations. The Phase 6.0 gate firesNtimes forNnon-skipped solutions inDEPLOYMENT_ORDER. If you find yourself proceeding from iteration 1's success directly to iteration 2'sDeployPackageAsyncwithout a fresh Phase 6.0 prompt, you have skipped the gate.
In MULTI_PIPELINE_MODE (v2 — legacy): The selected stage label (e.g., "Staging") is matched against each pipeline's stages[] — each pipeline has its own stageId for the same target environment. All subsequent phases (validate, deploy, poll) are looped over PIPELINES_LIST in order:
- Loop iteration i: use
pipelines[i].stageIdwhere stage label matchesSELECTED_STAGE.name,pipelines[i].solutionName, etc. - If any iteration fails (validation or deployment), halt the loop and report which pipeline failed and which were already deployed.
- Write one
docs/alm/last-deploy.jsonat the end summarizing all pipeline runs for this stage. Record per-pipelinestatus(Succeeded/Failed/NotAttempted) so a retry can tell which ones still need to run.
⚠ Per-iteration gate firing also applies here. Same rule as MULTI_RUN_MODE: each pipeline in the loop gets its own Phase 4 / 5 / 6.0 / 6.1 / 6.2 sequence. The Phase 6.0 consent gate fires once per pipeline. Do NOT batch.
Partial-deploy risk. When the loop halts (e.g.,
Coresucceeded,WebAssetsfailed), the target environment is left in a mixed state — there is no automatic rollback of solutions that already imported. The per-solution (v3) or per-pipeline (v2)statusindocs/alm/last-deploy.jsonis the source of truth for what landed. When the user re-runsdeploy-pipelineafter fixing the failure, the loop iterates all entries again from the start; rely on the solution-import idempotency (same version = no-op) rather than skipping. Warn the user of this before starting a multi-solution deploy to production.
Check docs/alm/last-deploy.json — if the last deployment to this stage failed, warn the user:
"The last deployment to
{stageName}had status: Failed. Would you like to retry? 1. Yes, retry / 2. No, cancel"
Phase 2.5 — Pre-flight: target env blocked-attachments check
Power Pages code-site solutions almost always contain .js bundle chunks (Vite/Rollup output) as Web File components. If the target env's blockedattachments setting includes .js, ImportSolutionAsync will reject every web-file write — typically 50-75 minutes into an import for sites with thousands of bundle chunks (real-world: a Content solution failed at 3,909 rejected .js files on Staging after the same issue had already been fixed on Dev). The reactive Phase 7.6 handler will detect this and offer unblock-and-retry, but the user has already burned an hour. This pre-flight catches it in 10 seconds.
Skip rule. This check is for Power Pages projects only. Skip when powerpages.config.json has no websiteRecordId (non-Power-Pages ALM run — pure data-model solution, etc.). Skip when the user's plan/manifest indicates no solution being deployed has Web File componentType (rare in code-site projects but possible for back-end-only solutions).
Detection signal. In MULTI_RUN_MODE / MULTI_PIPELINE_MODE: read .solution-manifest.json and check whether any entry in solutions[] has componentTypes including "Web File". In single-solution mode: assume true for any Power Pages project (the umbrella solution carries web files).
Steps:
Switch PAC CLI context to the target environment so
fix-blocked-attachments.jsqueries the right env:pac env select --environment "{SELECTED_STAGE.targetEnvironmentUrl}"Run the helper in dry-run mode to detect the current state:
node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/fix-blocked-attachments.js" \ --envUrl "{SELECTED_STAGE.targetEnvironmentUrl}" \ --extensions js,css \ --dry-runCapture the output as JSON. Inspect
wasBlocked[]:wasBlocked: []→ target env doesn't block the relevant extensions. Switch PAC CLI back to source (pac env select --environment "{sourceEnvUrl}") and proceed to Phase 3. No prompt, no noise.
🚦 Gate (consent · deploy-pipeline:2.5.blocked-attachments): Pre-flight — modify target env's
blockedattachmentssecurity setting (tenant-wide impact). Reversible from PPAC. Skipping costs 50–75 min of wasted import.wasBlocked: ["js"]or includes other media-relevant extensions → the deployment WILL fail mid-import. Prompt the user immediately viaAskUserQuestion(do NOT bury this in chat — it MUST gate Phase 3 progression):Pre-flight detected an issue. The target environment
{targetEnvName}currently blocks file types that this solution needs:{wasBlocked.join(', ')}. Power Pages code sites ship.jsbundle chunks as web files — if you proceed without unblocking, the deployment will run for ~50-75 minutes and then fail (the failure is recoverable via Phase 7.6's retry path, but the wasted time is not). The block can be removed in 10 seconds.Note: this modifies an environment-level security setting that affects all users of
{targetEnvName}. Reversible from PPAC → Environments →{targetEnvName}→ Settings → Product → Features → Blocked Attachments.Question Header Options Allow removing the block on {wasBlocked.join(', ')}for the{targetEnvName}environment so the deployment can proceed?Unblock attachments Yes — unblock these types and continue, No — proceed anyway (Phase 7.6 will catch the failure after deploy and prompt again), Cancel deploy
Branch on the answer:
Yes — unblock and continue: invoke
fix-blocked-attachments.jswithout--dry-run(same--extensions). Read the result and confirmchanged: true+removed[]is non-empty. Switch PAC CLI back to source (pac env select --environment "{sourceEnvUrl}"). Proceed to Phase 3. Record the unblock action in the eventualdocs/alm/last-deploy.jsonpreflightActions[]block so the deploy summary has an audit trail.No — proceed anyway: leave the setting unchanged. Switch PAC CLI back to source. Proceed to Phase 3. Tell the user clearly: "Continuing without unblocking. If the deployment fails on
AttachmentBlocked, Phase 7.6 will offer the same unblock prompt — but you'll have spent ~50-75 minutes getting there."Cancel deploy: stop cleanly. Do not modify any environment setting. Do not create the stage run. Tell the user how to re-invoke when ready.
Always switch PAC CLI back to the source environment before exiting this phase. Subsequent phases assume PAC points at the source unless they explicitly switch to the target.
Why pre-flight + reactive both exist. The reactive Phase 7.6 handler stays in place because (a) the pre-flight only inspects the env-level
blockedattachmentssetting — there are other rare blocked-attachment causes (per-table file column attachment policies) that only surface during the actual import; (b) the env's blocklist could change between pre-flight and import (rare but possible if another admin edits it concurrently); (c) backward compatibility with existing flows where the user invokeddeploy-pipelinedirectly and skipped Phase 2.5 via legacy SKILL.md. The two paths are complementary, not redundant.
Phase 3 — Resolve Pipeline Info
Call RetrieveDeploymentPipelineInfo to get the authoritative source environment ID and available solution artifacts:
GET {hostEnvUrl}/api/data/v9.1/RetrieveDeploymentPipelineInfo(DeploymentPipelineId={pipelineId},SourceEnvironmentId='{BAP_SOURCE_ENV_ID}',ArtifactName='{solutionName}')
Authorization: Bearer {HOST_TOKEN}
OData-MaxVersion: 4.0
OData-Version: 4.0
Accept: application/json
Where BAP_SOURCE_ENV_ID = the BAP GUID of the dev environment (from pac env list, stored in docs/alm/last-pipeline.json or available from pac env who).
Extract:
SourceDeploymentEnvironmentId— use as thedevdeploymentenvironmentbinding in the stage run. Store assourceDeploymentEnvironmentId.StageRunsDetails[].DeploymentStage— confirms available stages and their IDsEnableAIDeploymentNotes— store asAI_NOTES_ENABLED(bool)
Use solutionId from .solution-manifest.json as ARTIFACT_SOLUTION_ID and uniqueName as ARTIFACT_SOLUTION_NAME.
If
RetrieveDeploymentPipelineInforeturns 404 (older Pipelines package): use the navigation property to find the source deployment environment:GET {hostEnvUrl}/api/data/v9.1/deploymentpipelines({pipelineId})/deploymentpipeline_deploymentenvironment?$select=deploymentenvironmentid,name,environmenttypeFilter for
environmenttype = 200000000to get the source record. Usedeploymentenvironmentidas thesourceDeploymentEnvironmentId. For the artifact/solution list, usesourceDeploymentEnvironmentIdfromdocs/alm/last-pipeline.jsonandsolutionNamefrom.solution-manifest.jsonas fallbacks. Set a flagVALIDATE_PACKAGE_UNAVAILABLE = trueto skip Phase 4.2–4.3 and use the PAC CLI path in Phase 6.
Phase 3.5 — Pre-deploy Completeness Check
A pipeline's ValidatePackageAsync confirms the solution zip is importable on the target, but it does not tell you whether the solution zip itself covers every component that exists on the source site. Components added after setup-solution last ran (server logic, cloud flows, bots, env vars, etc.) can be silently left behind.
Run the shared site-inventory helper against the source (dev) environment:
node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/discover-site-components.js" \
--envUrl "{devEnvUrl}" --token "{DEV_TOKEN}" \
--siteId "{websiteRecordId from .solution-manifest.json}" \
--publisherPrefix "{publisherPrefix from .solution-manifest.json}" \
--solutionId "{solutionId from .solution-manifest.json}"
Parse stdout and evaluate missing.*. Before doing anything else, capture the pre-sync state so a post-sync re-confirmation gate can show what changed:
PRE_SYNC_VERSION = solutionManifest.solution.version // from .solution-manifest.json read in Phase 1
PRE_SYNC_MISSING = { siteComponents, siteLanguages, cloudFlows, envVarDefinitions, customTables, ... } // from the discovery stdout above
Then:
- All
missing.*empty → proceed to Phase 4.
🚦 Gate (progress · deploy-pipeline:3.5.completeness): Source solution incomplete vs live site. Sync first, deploy anyway (gap ships), or cancel.
Any non-empty → report a short summary ("Solution is missing {N} components"). Ask via
AskUserQuestion:"The source solution appears incomplete relative to the live site. What would you like to do?
- Run
/power-pages:setup-solutionnow (sync mode) — adopts missing components and bumps the version, then re-confirm with you before deploying (Recommended) - Deploy anyway — the missing components will not reach the target
- Cancel — I'll investigate first"
Option 1 — Sync first, then re-confirm before deploy:
- Invoke
/power-pages:setup-solution(auto-detects the existing manifest, enters sync mode, adopts missing components, bumps the version). Wait for completion. setup-solution's final refresh step writesLAST_SYNC_ATintodocs/.alm-plan-data.jsonso subsequentcheck-alm-plan.jscalls do NOT falsely flag the plan as stale just because the sync bumpedsolutions.modifiedonpastGENERATED_AT— the freshness reference becomesmax(GENERATED_AT, LAST_SYNC_AT). - Re-read
.solution-manifest.jsonand capturePOST_SYNC_VERSION = solutionManifest.solution.version. - Re-run the discovery helper. If any
missing.*remain non-empty, repeat the Phase 3.5 prompt above. - Otherwise compute
NEWLY_ADOPTEDas a per-category set difference betweenPRE_SYNC_MISSINGand the second discovery run'smissing.*(the items that disappeared are what setup-solution just adopted into the solution). Total count = sum of all category lengths.
🚦 Gate (progress · deploy-pipeline:3.5.post-sync): Post-sync re-confirm. Solution version bumped + components adopted — user inspects delta before deploy proceeds.
Re-confirm with the user before proceeding to Phase 4 — the solution about to ship is now different from what the user originally saw when they started the deploy. Use
AskUserQuestion:"Sync complete.
{solutionUniqueName} is now v{POST_SYNC_VERSION} (was v{PRE_SYNC_VERSION}) with {NEWLY_ADOPTED.total} newly-adopted components:
- {first 3-5 names by category — prefer high-signal categories: cloud flows, server logic, env var definitions, then site components}
- {if more remain:
+ {N} more across {category list}}
About to deploy this updated solution to {SELECTED_STAGE.name} ({targetEnvUrl}).
Continue with the deployment?"
Question Header Options Continue with the deployment? Post-sync approval Yes — deploy v{POST_SYNC_VERSION} to {SELECTED_STAGE.name} (Recommended), Pause — I want to review the new solution contents first, Cancel — abort the deploy - Yes → proceed to Phase 4 with the post-sync solution.
- Pause → exit deploy-pipeline cleanly with a short note ("Paused after sync. Re-run
/power-pages:deploy-pipelinewhen you're ready to ship v{POST_SYNC_VERSION} to {SELECTED_STAGE.name}.") so the user can inspect the synced manifest / Dataverse state and resume manually. Do not writedocs/alm/last-deploy.json— no deployment happened. Skip the skill-tracking call too. - Cancel → stop the skill. Same no-marker / no-tracking rule applies.
- Invoke
Option 2 — record the deliberate gap in
docs/alm/last-deploy.jsonunder aknownGapsfield so the audit trail is preserved.Option 3 — stop.
- Run
Why the post-sync gate exists: this skill is the gate that promotes a solution to staging or production — the moment of staging promotion is the last place to catch surprises. When sync mode runs mid-deploy, it produces a different solution version than the one the user had in mind when they invoked the skill. Re-confirming after sync gives the user an explicit chance to inspect the version bump and the list of newly-adopted components before they reach the target environment. The Phase 3.5 trigger is intentional; the post-sync re-confirmation is the safety on top of it. Same principle applies when this skill is invoked from
plan-almorchestration —plan-alm's plan approval (Phase 4) covers the pre-sync state; this gate covers the delta introduced by mid-deploy sync.
Why Phase 3.5 exists in the first place: the ALM-aware-by-default rule in
AGENTS.mdrequires the completeness check at every gate where a solution leaves its source environment.
Phase 3.6 — Parallel Validation Batch (MULTI_RUN_MODE only)
Skip this entire phase when
MULTI_RUN_MODE = false(single-solution mode, or legacyMULTI_PIPELINE_MODEv2). Single-solution and v2 each create + validate exactly one stage run inside Phase 4 below — there's nothing to parallelize. Resume at Phase 4.
This phase compresses the per-solution create-stage-run → ValidatePackageAsync → poll-validation-status chain by running all N solutions concurrently. ValidatePackageAsync does not acquire the env-level import lock that pins DeployPackageAsync to serial execution — it's a structural check on the solution package and per-stage-run state, so the platform happily processes N validations in parallel. For a typical 5-solution split with 60–180s validations, this drops the validation phase from N × ~120s (≈10 min) to roughly the slowest single validation (≈3 min).
The deploy phase (6.1 / 6.2) remains strictly serial — see the Design rationale callout in Phase 2.
3.6.1 Build the input file.
Filter DEPLOYMENT_ORDER down to entries that need validation:
- Include: any entry whose
status !== "SkippedEmpty"ANDisFutureBuffer !== true. - Exclude:
SkippedEmptyandisFutureBuffer: trueentries — the Future buffer is a reserved 0-component placeholder and there's nothing to validate.
Resolve each remaining entry's solutionId from .solution-manifest.json (schemaVersion: 2 solutions[]) if not already on the deployment-order entry. Write to a tmp file:
node -e "require('fs').writeFileSync('./docs/alm/.validation-batch.json', JSON.stringify({{VALIDATION_SPECS}}))"
Where {{VALIDATION_SPECS}} is the array [{ solutionUniqueName, solutionId }, …].
3.6.2 Run the batch validator.
node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/validate-stage-runs-batch.js" \
--hostEnvUrl "{hostEnvUrl}" \
--token "{HOST_TOKEN}" \
--pipelineId "{pipelineId}" \
--stageId "{SELECTED_STAGE.stageId}" \
--sourceDeploymentEnvironmentId "{sourceDeploymentEnvironmentId}" \
--solutionsFile ./docs/alm/.validation-batch.json
Capture stdout as JSON: const batch = JSON.parse(output). Delete the tmp file (./docs/alm/.validation-batch.json) — it's transient. Build VALIDATED_STAGE_RUNS = { [solutionUniqueName]: { stageRunId, validationResults } } from batch.results.
If
VALIDATE_PACKAGE_UNAVAILABLEpropagates (any per-solutionerrormatchesValidatePackageAsync not available on this Pipelines package): setVALIDATE_PACKAGE_UNAVAILABLE = trueglobally, skip the rest of Phase 3.6 (the stage runs created so far stay around — they're harmless validated-but-not-deployed records), and proceed to Phase 4 in single-solution-fallback shape, which routes each iteration through thepac pipeline deployCLI fallback in Phase 6. This is the same code path the older Pipelines package versions take.
3.6.3 Branch on the batch outcome.
batch.allPassed |
batch.pendingApproval |
batch.failed + batch.timedOut |
Behavior |
|---|---|---|---|
true |
0 | 0 | All validations passed. Report a single line: "Validated {N} solution(s) in parallel — all passed." Proceed to Phase 4. |
false |
> 0 | 0 | One or more validations are awaiting approval. See 3.6.4 (Pending Approval batch handling) below. |
false |
— | > 0 | One or more validations failed or timed out. See 3.6.5 (Halt on batch validation failure) below. |
3.6.4 Pending Approval batch handling.
🚦 Gate (pause · deploy-pipeline:3.6.batch-pending-approval): External wait — one or more solutions hit
stagerunstatus=200000005(Pending Approval) during parallel validation. User approves all in PPAC, then we re-poll. Cancel leaves N validated-but-pending stage runs on the host (the user can either approve them later and re-invoke, or cancel them in PPAC). Fires once per batch — not once per pending solution.
Surface the affected solutions in a single message (not per-solution) and pause. Use AskUserQuestion:
"Validation for
{batch.pendingApproval}of{batch.total}solution(s) is awaiting approval before it can complete: {bulleted list ofresult.solutionUniqueNamewherestatus === 'PendingApproval'}Approve all of them in Power Platform:
make.powerapps.com→ Solutions → Pipelines → for each stage run listed above → Approve. Then return here.The other
{batch.succeeded}solution(s) already validated successfully — they'll deploy after you approve."
Question Header Options Approvals complete? Batch validation approval Yes — I approved all of them; re-poll, No — cancel the deploy
Yes: re-run
validate-stage-runs-batch.jsin--rePollmode. The helper accepts existing stage run IDs and skips thecreate-stage-run+ValidatePackageAsynccalls — it just runs the poll-and-probe pattern against eachstageRunId. After user approval, the stage run transitions200000005 (PendingApproval) → 200000006 (Validating) → 200000007 (ValidationSucceeded); the helper's probe correctly distinguishes "still pending" (the user clicked Yes prematurely) from "real timeout" so the agent doesn't have to interpret a generic timeout error.Build a tmp file with only the previously-pending entries (carry
stageRunIdfrom the original batch result):node -e "require('fs').writeFileSync('./docs/alm/.repoll-batch.json', JSON.stringify({{PENDING_SPECS_WITH_STAGERUNIDS}}))" node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/validate-stage-runs-batch.js" \ --hostEnvUrl "{hostEnvUrl}" \ --token "{HOST_TOKEN}" \ --rePoll \ --solutionsFile ./docs/alm/.repoll-batch.jsonWhere
{{PENDING_SPECS_WITH_STAGERUNIDS}}is the array[{ solutionUniqueName, solutionId, stageRunId }, …]filtered to entries withstatus === 'PendingApproval'from the original batch.Capture stdout as JSON; delete the tmp file (
./docs/alm/.repoll-batch.json). Merge the updated outcomes intoVALIDATED_STAGE_RUNSkeyed bysolutionUniqueName. Then branch on the rePoll batch's tally:- All succeeded → proceed to Phase 4 with the full set of validated stage runs.
- One or more still PendingApproval → fire the same gate again (the approval hadn't propagated; the user gets a fresh "approve in PPAC, then re-poll" prompt). Loop until either all approved or the user cancels.
- One or more
Failed/Timeout/Error→ fall through to 3.6.5 (treat as batch validation failure).
No: stop cleanly. The validated stage runs and the pending stage runs remain on the host; re-invoking the skill picks up where this left off (the user can approve and re-run).
3.6.5 Halt on batch validation failure.
🚦 Gate (plan · deploy-pipeline:3.6.batch-validation-failed): One or more solutions failed validation in the parallel batch. Surface per-solution
validationResultsfor the failing entries, and let the user decide whether to abort the entire deploy or proceed with only the succeeded solutions (advanced — leaves a known dependency gap on the target). Cancel leaves N validated stage runs on the host; the user can clean them up in PPAC or re-invoke after fixing the source.
Surface the failing entries with their validationResults (which is the double-encoded JSON string from the Dataverse validationresults field — JSON.parse it twice when displaying to extract SolutionValidationResults[].Message). Use AskUserQuestion:
"
{batch.failed + batch.timedOut}of{batch.total}solution(s) failed parallel validation:{for each failing result: a short block with
solutionUniqueName,status, topMessagefromvalidationResults}The other
{batch.succeeded}solution(s) passed and have validated stage runs ready to deploy. Deploying only the succeeded subset risks leaving a dependency gap on the target — e.g. shipping_Contentwithout its prerequisite_Foundationproduces broken site behavior. Recommended: abort, fix the source, re-invoke.What would you like to do?"
Question Header Options Next step? Batch validation failed Abort the entire deploy (Recommended), Deploy only the succeeded subset (advanced — accept the gap), Cancel and investigate
- Abort / Cancel: stop cleanly. Write a minimal
docs/alm/last-deploy.jsonwithstatus: "ValidationFailed"per failing solution andstatus: "NotAttempted"for the rest, so the next invocation can see what happened. Do not callDeployPackageAsync. - **Deploy only
…(truncated)