deploy-pipeline

SkillCloud & infra

Triggers a Power Platform Pipeline deployment run for a Power Pages solution. Selects a target stage, validates the package, optionally configures deployment settings (environment variables, connection references), then deploys and polls for completion. Use when asked to: "deploy pipeline", "run pipeline", "trigger deployment", "deploy to staging", "deploy to production", "run power platform pipeline", "deploy solution via pipeline", "promote solution", "push to staging", "push to production".

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the deploy-pipeline skill

What this skill tells your AI

The instructions your AI receives, as published by microsoft/power-platform-skills in plugins/power-pages/skills/deploy-pipeline/SKILL.md and read by ahel’s review.

Plugin check: Run node "${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-pipeline first to create the pipeline configuration.

Refer to ${PLUGIN_ROOT}/references/cicd-pipeline-patterns.md for 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 DefaultCustomPipelinesHostEnvForTenant setting. Without this configuration, pac pipeline deploy will fail. The setup-pipeline skill creates the pipeline definition in the host; this admin step connects the dev environment to that host.

  • docs/alm/last-pipeline.json exists (created by setup-pipeline)
  • .solution-manifest.json exists
  • Azure CLI logged in (az account show succeeds)
  • PAC CLI logged in (pac env who succeeds)

Phases

Phase 0 — ALM plan gate

plan-alm is 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 of deploy-pipeline bypasses 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.jsonPLAN_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 "${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.

ResultBehavior
deferred: trueThe user has explicitly deferred ALM for this project (.alm-deferred marker present). Pass through silently to Phase 1 — do not nag.
exists: falseThe user hasn't run plan-alm yet. See Step 3.
exists: true, stale: falsePlan 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-alm builds 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.js returns exists:false. Helper-script-backed.

AskUserQuestion:

QuestionHeaderOptions
Run /power-pages:plan-alm first?ALM plan gateYes — 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. It builds the plan and returns — plan-alm is a planner and does not deploy. This skill then re-runs the Phase 0 check (now exists:true) and proceeds to Phase 1.
  • Continue without a plan → set BYPASSED_PLAN_GATE = true and 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-running plan-alm will refresh the analysis."

🚦 Gate (intent · deploy-pipeline:0.stale-plan): Fail-closed entry gate when check-alm-plan.js returns stale:true. Helper-script-backed.

AskUserQuestion:

QuestionHeaderOptions
Refresh the plan first?ALM plan freshnessRefresh — 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 = true and 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:

  1. "Verify prerequisites"
  2. "Select target stage"
  3. "Resolve pipeline info"
  4. "Validate package" — in MULTI_RUN_MODE this 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
  5. "Configure deployment settings"
  6. "Deploy and monitor"
  7. "Write deployment record"

Steps:

  1. Resolve the project's configured environment URL first, then assert PAC is actually connected to it. verify-alm-prerequisites.js resolves the env from PAC's ambient org context (pac env who), which is not guaranteed to match the project. If PAC has drifted to another environment (duplicate active pac auth profiles, or an external process flipping the active env), the skill would silently run discovery — and later pac pipeline deploy — against the wrong environment (potentially PROD). Assert the match and hard-stop on mismatch rather than pinning --envUrl: pinning would correct only the Dataverse-API calls while later PAC-CLI operations still follow the drifted context, so asserting that PAC itself is on the right env is the safer gate.

    Read the project's recorded env URL (first match wins): .solution-manifest.json → top-level environmentUrl, else powerpages.config.jsonenvironmentUrl. Store as CONFIGURED_ENV_URL. (Both fields are top-level environmentUrl strings; declarative/EDM sites have no powerpages.config.json, so the manifest is the source there.)

    Pass --expectedEnvUrl only when CONFIGURED_ENV_URL actually resolved to a URL. Use the first form when a recorded env URL exists, the second when neither file records one — do not pass an empty or unresolved --expectedEnvUrl "{CONFIGURED_ENV_URL}":

    # CONFIGURED_ENV_URL resolved (recorded in manifest/config) — assert PAC is on it:
    node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --require-manifest --expectedEnvUrl "{CONFIGURED_ENV_URL}"
    
    # Neither file records an env URL — omit the flag, fall back to the pac-context default:
    node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --require-manifest
    

    --expectedEnvUrl makes the helper compare PAC's resolved env (origin-only) against CONFIGURED_ENV_URL and exit non-zero with an "Environment mismatch: PAC CLI is connected to {X} but this project targets {Y} — run pac env select …" error on mismatch. (As a safety net the helper skips the assertion if the value isn't a parseable env URL — an empty string or an unsubstituted placeholder won't hard-stop — but prefer omitting the flag outright when there's no recorded URL.) Capture output as JSON; extract .envUrl (store as devEnvUrl) and .token (store as DEV_TOKEN). If the script exits non-zero, stop and surface the error — it indicates an env mismatch, or that az login / pac auth / WhoAmI failed.

  2. Run detect-project-context.js to read project config and solution manifest:

    node "${PLUGIN_ROOT}/scripts/lib/detect-project-context.js"
    

    Capture output as JSON; extract .solutionManifest (store as solutionManifest), .siteName (store as siteName), and .websiteRecordId. If solutionManifest is null, continue — the manifest is not strictly required at this step (solution info will come from docs/alm/last-pipeline.json).

  3. Locate docs/alm/last-pipeline.json — if not found, stop and advise running /power-pages:setup-pipeline first.

    Manifest version check:

    • If schemaVersion === 3, set MULTI_RUN_MODE = true and store deploymentOrder[] as DEPLOYMENT_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 in order. This is the current recommended layout.
    • If schemaVersion === 2 (legacy), set MULTI_PIPELINE_MODE = true and store pipelines[] as PIPELINES_LIST. The skill falls back to the older "loop over N separate deploymentpipelines records" behavior. Advise the user to re-run setup-pipeline to migrate to v3.
    • Otherwise read pipelineId, pipelineName, hostEnvUrl, sourceDeploymentEnvironmentId, solutionName, stages[] (single-solution mode — existing behavior).

    In MULTI_RUN_MODE, resolve solutionName + solutionId per iteration of DEPLOYMENT_ORDER. Entries where status === "SkippedEmpty" (typically the {Prefix}_Future buffer) are short-circuited — no stage run is created for them. The single pipelineId / hostEnvUrl / sourceDeploymentEnvironmentId apply to every run.

    In MULTI_PIPELINE_MODE (legacy v2), resolve solutionName per pipeline in the loop (not globally). All pipelines share the same hostEnvUrl and sourceDeploymentEnvironmentId.

  4. Acquire host environment token:

    az account get-access-token --resource "{hostEnvOrigin}" --query accessToken -o tsv 2>/dev/null
    

    Where hostEnvOrigin = scheme + host of hostEnvUrl. Store as HOST_TOKEN. If acquisition fails, stop with instructions to check Azure CLI auth.

  5. If solutionManifest is available, read solutionManifest.solution.solutionId and solutionManifest.solution.uniqueName from the detected context. Otherwise, use solutionName from docs/alm/last-pipeline.json.

  6. Report: "Pipeline: {pipelineName}. Solution: {solutionName}. Available stages: {stage names}."

Phase 1.5 — Ground in current Pipelines deployment documentation

Reference: ${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.

  1. Run microsoft_docs_search with the query: Power Platform Pipelines stage run validation ValidatePackageAsync DeployPackageAsync approval.
  2. 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 via microsoft_docs_fetch.
  3. Extract a one-paragraph summary of what Microsoft Learn currently says about stage-run lifecycle, validation outcomes, approval-gate workflow, and deploymentsettingsjson overrides. Compare against ${PLUGIN_ROOT}/references/cicd-pipeline-patterns.md and flag any divergence (new status codes, changed stagerunstatus terminal values, new approval-gate API).
  4. 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.:

  1. Deploy to Staging → {stagingEnvUrl}
  2. 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_ORDER has N entries (e.g. Core → WebAssets → Future for a 2-solution split with a future buffer), the loop runs one solution at a time, in order ascending, halt-on-first-failure. This is intentional. Four constraints stack up and make parallel DeployPackageAsync calls actively harmful, not just non-beneficial:

  1. Dataverse import lock at the target env. ImportSolutionAsync takes an env-level lock — only one solution can actively import at a time per environment. Even if the skill fired N DeployPackageAsync calls in parallel, the host would queue them and run them serially anyway. The wall-clock win for parallel deploys is effectively zero.
  2. 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 the powerpagesite record in Core) all encode strict ordering. Re-ordering breaks the import — a flow that references a table not yet in the target fails with MissingDependency.
  3. Per-iteration consent gates. Phase 6.0 (deploy-pipeline:6.0.final-consent) fires before EVERY DeployPackageAsync. 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 concurrent AskUserQuestions, neither of which the harness supports.
  4. Clean failure handling. Serial + halt-on-first-failure means docs/alm/last-deploy.json records per-solution status cleanly. 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 — ValidatePackageAsync does 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:

  1. Phase 3.6 runs first (once, before the loop) — fans out create-stage-run + ValidatePackageAsync + poll-validation-status for every non-skipped solution in parallel. Halts the entire deploy if any solution fails validation. Stores the per-solution stageRunId in VALIDATED_STAGE_RUNS so the serial deploy loop can reuse them.
  2. For each entry in DEPLOYMENT_ORDER where status !== "SkippedEmpty": resolve its solutionUniqueName + solutionId, retrieve its stageRunId from VALIDATED_STAGE_RUNS[solutionUniqueName], set ARTIFACT_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.
  3. If any iteration fails (deployment), halt the loop and report which solution failed and which had already landed.
  4. Write one docs/alm/last-deploy.json at the end summarizing all runs for the selected stage. Record per-solution status (Succeeded / Failed / NotAttempted / SkippedEmpty) plus the shared pipelineId.

⚠ 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, --stage argument, the previous iteration's "Deploy now") as covering subsequent iterations. The Phase 6.0 gate fires N times for N non-skipped solutions in DEPLOYMENT_ORDER. If you find yourself proceeding from iteration 1's success directly to iteration 2's DeployPackageAsync without 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:

  1. Loop iteration i: use pipelines[i].stageId where stage label matches SELECTED_STAGE.name, pipelines[i].solutionName, etc.
  2. If any iteration fails (validation or deployment), halt the loop and report which pipeline failed and which were already deployed.
  3. Write one docs/alm/last-deploy.json at the end summarizing all pipeline runs for this stage. Record per-pipeline status (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., Core succeeded, WebAssets failed), 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) status in docs/alm/last-deploy.json is the source of truth for what landed. When the user re-runs deploy-pipeline after 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:

  1. Switch PAC CLI context to the target environment so fix-blocked-attachments.js queries the right env:

    pac env select --environment "{SELECTED_STAGE.targetEnvironmentUrl}"
    
  2. Run the helper in dry-run mode to detect the current state:

    node "${PLUGIN_ROOT}/scripts/lib/fix-blocked-attachments.js" \
      --envUrl "{SELECTED_STAGE.targetEnvironmentUrl}" \
      --extensions js,css \
      --dry-run
    
  3. Capture 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 blockedattachments security 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 via AskUserQuestion (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 .js bundle 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.

      QuestionHeaderOptions
      Allow removing the block on {wasBlocked.join(', ')} for the {targetEnvName} environment so the deployment can proceed?Unblock attachmentsYes — unblock these types and continue, No — proceed anyway (Phase 7.6 will catch the failure after deploy and prompt again), Cancel deploy
  4. Branch on the answer:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
868
Forks
177
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
deploy-pipeline-microsoft
Source
github.com/microsoft/power-platform-skills