setup-pipeline
SkillCloud & infraSets up a Power Platform Pipeline for automated Power Pages deployments. Power Platform Pipelines is Microsoft's native CI/CD tool built into the Power Platform — no external infrastructure required. Use when asked to: "set up ci/cd", "create pipeline", "setup pipeline", "set up power platform pipelines", "create power pipelines", "automate deployments", "set up automated deployment", "create deployment pipeline", "use power pipelines". Also handles: "set up github actions" or "set up azure devops pipeline" (shows coming-soon guidance for those platforms).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the setup-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/setup-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.
setup-pipeline
Sets up a Power Platform Pipeline for automated Power Pages solution deployments. Creates the pipeline configuration directly in Dataverse using the PP Pipelines OData API — no YAML files, no external CI/CD infrastructure needed.
GitHub Actions and Azure DevOps Pipeline options are shown in the platform menu as coming soon.
Refer to
${PLUGIN_ROOT}/references/cicd-pipeline-patterns.mdfor all HAR-confirmed API patterns used in this skill.
Prerequisites
powerpages.config.jsonexists in the project root.solution-manifest.jsonexists (solution must be created first viasetup-solution)- Azure CLI logged in (
az account showsucceeds) - PAC CLI logged in (
pac env whosucceeds) - A Power Platform environment with Pipelines package installed (the "host" environment)
Phases
Phase 0 — ALM plan gate
plan-almis the front door. When the user expresses an ALM intent (promote / ship / deploy / set up CI-CD / move to staging / push to prod), the orchestrator (/power-pages:plan-alm) should run first. This Phase 0 enforces that and is meant to fail closed when there's no plan, not to be a one-time check the user can dismiss forever.
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 "${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 (PP Pipelines vs Manual export/import), and orchestrates the right skills (including this one) in the right order. Want me to run plan-alm now?"
🚦 Gate (intent · setup-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 know what I'm doing), Cancel |
- Yes (Recommended) → invoke
/power-pages:plan-alm. It builds the plan and returns —plan-almis a planner and does not deploy. This skill then re-runs the Phase 0 check (nowexists:true) and proceeds to Phase 1. - Continue without a plan → set
BYPASSED_PLAN_GATE = trueand proceed to Phase 1. - 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}). Components may have changed. Re-runningplan-almwill refresh the analysis and the rendered HTML."
🚦 Gate (intent · setup-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.
Why this gate exists. Direct invocation of this skill bypasses the orchestrator's pre-deploy completeness check, host-resolution decision, deployment-strategy selection, and rendered HTML plan. Users who run setup-pipeline directly often miss components that should have been added to the solution, miss the asset advisory for large web files, or build a pipeline against the wrong host environment. The gate ensures plan-alm either ran (so all of those decisions are surfaced and recorded) or the user explicitly chose to bypass it.
Phase 1 — Detect Project Context
Create all tasks upfront at the start of this phase.
Tasks to create:
- "Detect project context"
- "Select CI/CD platform"
- "Confirm pipeline configuration"
- "Run preflight checks"
- "Create deployment environments"
- "Create pipeline and stages"
- "Verify and write artifacts"
Steps:
-
Read project context using
detect-project-context.js:node "${PLUGIN_ROOT}/scripts/lib/detect-project-context.js"Capture output as JSON; extract
.siteName(store assiteName),.websiteRecordId,.environmentUrl(store asdevEnvUrl), and.solutionManifest(store assolutionManifest).devEnvUrlisnullfor declarative / data-model (EDM) sites —detect-project-context.jsreads the env URL only frompowerpages.config.json, which those sites don't have. Do not treat a nulldevEnvUrlas an error here; Step 2 resolves the authoritative dev env URL frompac env who. IfsiteNameis absent, stop and advise running/power-pages:create-sitefirst — a downloaded/deployed site (code or declarative) always resolves asiteName(frompowerpages.config.jsonor.powerpages-site/website.yml), so a missingsiteNamemeans there is no site checked out here, not merely "nopowerpages.config.json". IfsolutionManifestis null (no.solution-manifest.json), stop and advise running/power-pages:setup-solutionfirst.Manifest version check:
- If
solutionManifest.schemaVersion === 2(multi-solution layout), setMULTI_SOLUTION_MODE = trueand storesolutionManifest.solutions[]asSOLUTIONS_LIST. See Phase 6b — a SINGLE pipeline ships all solutions through per-solution stage runs (the pre-v1.3.x "one pipeline per solution" layout was reverted because it cluttered the Pipelines UI). - If
schemaVersionis absent or1(single solution), readsolutionManifest.solution.uniqueNameandsolutionManifest.solution.solutionId. One pipeline will be created (existing flow).
- If
-
Run
verify-alm-prerequisites.jsto confirm PAC CLI auth, acquire a token, and verify API access. Pass--envUrlonly whendevEnvUrlis non-null (code sites). WhendevEnvUrlis null (declarative / data-model sites from Step 1), omit--envUrlentirely — do not pass--envUrl "null"or an empty value:# Code sites — devEnvUrl resolved from powerpages.config.json in Step 1: node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --envUrl "{devEnvUrl}" # Declarative / data-model (EDM) sites — devEnvUrl is null, omit the flag: node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js"verify-alm-prerequisites.jstreats--envUrlas optional and resolves the environment frompac env whowhen it's omitted (the flag only overrides the PAC CLI env). Capture output as JSON; extract.envUrland.token(store asDEV_TOKEN), then setdevEnvUrl = .envUrl— this verified value (frompac env who) is the authoritative dev env URL for every later step: it backfills the null for declarative sites and confirms it for code sites. If.envUrlis still empty after this, stop and advise the user to runpac auth create/ select an environment (pac org select) before retrying. -
Run silently:
node "${PLUGIN_ROOT}/scripts/lib/list-environments.js"Store the JSON array as
ENV_LIST(entries:{ displayName, environmentId, environmentUrl, uniqueName, active }). This helper parsespac env list— the oldpac env list --output jsonis invalid on current PAC CLI (pac env listonly accepts--filter). It prints[]and exits 0 when PAC is unauthenticated, so this step degrades gracefully. -
Resolve the Pipelines host via
ensure-pipelines-host-detect.js(the same flow/power-pages:ensure-pipelines-hostruns internally — it reads any cacheddocs/alm/last-host-check.json, then walks the resolution order: org-setting binding → BAP env GET → tenant default custom host → tenant-wide enumeration. Read-only; never prompts the user):BAP_TOKEN=$(az account get-access-token --resource "https://service.powerapps.com/" --query accessToken -o tsv) node "${PLUGIN_ROOT}/scripts/lib/ensure-pipelines-host-detect.js" \ --envUrl "{devEnvUrl}" \ --token "{DEV_TOKEN}" \ --userId "{userId}" \ --bapToken "{BAP_TOKEN}" \ --projectRoot "."Capture stdout as JSON:
const hostResult = JSON.parse(output). ReadhostResult.resolutionStatus,hostResult.finalHostEnvUrl,hostResult.ready.Branch on
resolutionStatus:AvailableUsingPlatformHost/AvailableUsingCustomHost/AvailableUsingCustomHostByAdminDefault— host is already established andready: true. StoreHOST_ENV_URL = hostResult.finalHostEnvUrland continue. Phase 3 confirms with the user.AvailableUnboundCustomHost/MultipleUnboundCustomHosts/PlatformHostExistsUnbound/NoHost— no host bound to the dev env. Delegate to/power-pages:ensure-pipelines-hostso the user can reuse an existing host or provision a new Custom Host (D365_ProjectHosttemplate). Tell the user: "No Pipelines host bound to{devEnvUrl}. Invoking/power-pages:ensure-pipelines-hostto set one up — it will run a tenant-wide search for existing hosts and offer to provision a new Custom Host if none are found." After the sub-skill completes, re-readdocs/alm/last-host-check.json; captureHOST_ENV_URL = finalHostEnvUrlonly if the new marker hasready: true. If the user cancelled the sub-skill, stop this skill — no pipeline can be created without a host.CannotRedirect— stop with the specific tenant-misconfiguration error fromhostResult.warnings[0]. Tell the user: "This tenant'sDefaultCustomPipelinesHostEnvForTenantsetting and the source env'sProjectHostEnvironmentIdorg setting disagree — only a Power Platform admin can resolve."OrgSettingStale— stop and surface the warning: "ProjectHostEnvironmentIdon{devEnvUrl}points at a host env that is no longer visible (deleted, disabled, or you lack access). Clear the org setting via PPAC or contact the env owner."PermissionDenied— stop and surface the warning: "Caller lacks BAP read access on the env{devEnvUrl}is bound to. Contact the host env owner for at leastDeployment Pipeline Useraccess."
Why this replaces the old
discover-pipelines-host.jscall: that helper only checked the tenant-levelDefaultCustomPipelinesHostEnvForTenantsetting (one of four resolution signals).ensure-pipelines-host-detect.jswalks the full resolution order the Power Apps UI uses (mirrorsProjectHostProvider.tsx), so we agree with the UI in every case — including the previously-undetectedAvailableUnboundCustomHostcase where a Custom Host exists in the tenant but the source env hasn't been bound yet. Seereferences/cicd-pipeline-patterns.mdfor the full state matrix. -
Check for existing
docs/alm/last-pipeline.json. If found, read its contents. -
Report findings: "Project:
{siteName}. Solution:{uniqueName}. Dev env:{devEnvUrl}. Host env:{HOST_ENV_URL ?? 'pending — will be ensured next'}({hostResult.resolutionStatus}). Existing pipeline: found/not found."
🚦 Gate (plan · setup-pipeline:1.existing-pipeline): Existing
docs/alm/last-pipeline.jsonfound — overwrite, review first, or cancel. No Dataverse write yet.
If an existing docs/alm/last-pipeline.json is found, ask via AskUserQuestion:
"A pipeline configuration already exists for
{pipelineName}(created {createdAt}). How would you like to proceed?
- Overwrite — create a new pipeline, replacing the marker
- Review existing setup first, then decide
- Cancel"
- If Review: display the existing
docs/alm/last-pipeline.jsoncontents, then ask again with the same 3 options. - If Cancel: stop the skill and inform the user no changes were made.
- If Overwrite: proceed.
Phase 1.5 — Ground in current Pipelines 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.
- Run
microsoft_docs_searchwith the query:Power Platform Pipelines setup OData API host environment deploymentenvironments. - Fetch
https://learn.microsoft.com/en-us/power-platform/alm/pipelines(and at most one sister page on host setup or pipeline creation) in parallel viamicrosoft_docs_fetch. - Extract a one-paragraph summary of what Microsoft Learn currently says about Pipelines host resolution,
deploymentenvironments/deploymentpipelines/deploymentstagesschema, and pipeline lifecycle. Compare against${PLUGIN_ROOT}/references/cicd-pipeline-patterns.mdand flag any divergence (new fields, deprecated APIs, changed validation status codes). - 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 5 (Register Environments with the Pipelines Host).
Phase 2 — Select CI/CD Platform
🚦 Gate (plan · setup-pipeline:2.platform): Pick CI/CD platform — PP Pipelines (full) vs GitHub Actions / ADO (coming soon stubs).
Ask user via AskUserQuestion:
"Which CI/CD platform do you want to use?
- Power Platform Pipelines — Microsoft's native deployment pipeline. No external infrastructure needed. (Recommended)
- GitHub Actions — Coming soon
- Azure DevOps Pipeline — Coming soon"
If the user passed power-platform, github, or ado as an argument, skip this question and use the provided value.
Store the selection as PLATFORM.
If github or ado selected → display the Coming Soon path and stop.
Power Platform Pipelines Path
Phase 3 — Confirm Pipeline Configuration
Before asking any questions, assemble what was auto-detected:
| Setting | Auto-detected value |
|---|---|
| Site name | {siteName} from powerpages.config.json |
| Solution unique name | {uniqueName} from .solution-manifest.json |
| Dev environment URL | {devEnvUrl} from pac env who |
| Host environment URL | {HOST_ENV_URL} from ensure-pipelines-host-detect.js (resolved in Phase 1 step 4) |
| BAP environment ID (dev) | From pac env list |
🚦 Gate (plan · setup-pipeline:3.config): Confirm auto-detected pipeline configuration — pipeline name, host env, target envs. Cancel exits before any Dataverse write to the host.
Ask user via AskUserQuestion with pre-filled values:
"I've gathered the following pipeline configuration. Please confirm or correct:
- Pipeline name:
{siteName} Pipeline(can change)- Source (Dev) environment:
{devEnvUrl}- Host environment (where Pipelines is installed):
{HOST_ENV_URL}(resolved in Phase 1 — should always be present at this point;ensure-pipelines-hostwould have stopped the skill otherwise)- Solution to deploy:
{uniqueName}- Target environments: How many? (Dev → Staging / Dev → Staging → Production)"
Collect from user:
PIPELINE_NAME(default:{siteName} Pipeline)HOST_ENV_URL(confirm — already resolved in Phase 1; user can override only if they want to point at a different host they administer, in which case re-run/power-pages:ensure-pipelines-hostfirst to validate it)- Target environment count and URLs (
STAGING_ENV_URL,PROD_ENV_URLif applicable) - BAP environment IDs for each target (from
pac env list— pre-fill if found, otherwise ask)
Store HOST_TOKEN by running:
az account get-access-token --resource "{hostEnvOrigin}" --query accessToken -o tsv
Present a final confirmation summary and ask user to approve before proceeding.
Phase 4 — Preflight Checks
Use Node.js https module for all Dataverse calls (curl has encoding issues on Windows).
4.1 Verify host environment has Pipelines installed:
GET {hostEnvUrl}/api/data/v9.1/deploymentpipelines?$top=0
Authorization: Bearer {HOST_TOKEN}
If response is 404 or returns an "unknown entity" error, stop and inform the user: "The selected host environment does not have Power Platform Pipelines installed. Please select a different environment or install the Pipelines package."
4.2 Verify solution exists in dev environment using verify-solution-exists.js:
node "${PLUGIN_ROOT}/scripts/lib/verify-solution-exists.js" \
--envUrl "{devEnvUrl}" \
--uniqueName "{uniqueName}" \
--token "{DEV_TOKEN}"
Capture output as JSON; check .found. If false: warn the user — the solution must be exported from dev before it can be deployed.
4.3 Check for existing pipeline with same name:
GET {hostEnvUrl}/api/data/v9.1/deploymentpipelines?$filter=name eq '{PIPELINE_NAME}'&$select=deploymentpipelineid&$top=1
Authorization: Bearer {HOST_TOKEN}
🚦 Gate (plan · setup-pipeline:4.3.name-conflict): A pipeline with the same name already exists in the host env. Pick: reuse the existing pipeline ID, or create a new one with a different name. Auto-reusing risks attaching to a pipeline owned by someone else; auto-overwriting loses their stage history.
Trigger: Phase 4.3 query returned a hit. Why we ask: Either a foreign pipeline gets its stages overwritten, or a duplicate pipeline gets created that pollutes the host env's pipeline list. Cancel leaves: Nothing — no Dataverse write yet.
If found: ask via AskUserQuestion whether to use the existing pipeline ID or create a new one with a different name.
4.4 Check blockedattachments on source + all target envs:
Power Pages code sites include .js files in their compiled output. If .js is in the env's blockedattachments setting, pac pages upload-code-site (on the source) and deploy-pipeline (on targets) will both fail with AttachmentBlocked. Run this on the source env and on every target env:
node "${PLUGIN_ROOT}/scripts/lib/fix-blocked-attachments.js" \
--envUrl "{envUrl}" \
--extensions js \
--dry-run
If wasBlocked is non-empty for any env, inform the user:
"
.jsfiles are blocked in{envUrl}. This will cause upload/deployment failures for Power Pages code sites. Remove the block? This modifies an environment-level security setting."
🚦 Gate (consent · setup-pipeline:4.4.blocked-attachments): Modify env-level
blockedattachmentssecurity setting (tenant-wide impact). Affects all users of the env, not just this skill. Reversible from PPAC. Fires PER ENV that has blocks. Phase 4.4 checks source + every target env; if M envs out of N have.js(or other media extensions) on the blocklist, the gate fires M times — once per env. Each env has its own security setting and its own group of affected makers. Yes for source does NOT cover staging; yes for staging does NOT cover production. Do NOT batch consent across envs.
Ask via AskUserQuestion: 1. Yes, remove block (recommended) / 2. Skip (I'll fix manually).
If approved, re-run without --dry-run to apply the change. If the user declines, record it as a warning — they'll need to fix it manually before deployment succeeds.
Report preflight results. If any critical check failed, stop with clear instructions. If warnings only, ask user to confirm before proceeding.
Phase 5 — Register Environments with the Pipelines Host
Register each environment (source + targets) with the Pipelines host by creating a deploymentenvironments row in the host's Dataverse. This is a metadata-only registration — the row is a pointer to an existing BAP environment, not a provisioning call. The environments themselves must already exist in BAP. The host validates that the referenced env is reachable and the caller has the right access (validationstatus flips Pending → Succeeded). Process source env first, then targets.
Use create-deployment-environment.js for each environment (dev source + each target):
node "${PLUGIN_ROOT}/scripts/lib/create-deployment-environment.js" \
--hostEnvUrl "{HOST_ENV_URL}" \
--token "{HOST_TOKEN}" \
--name "{siteName} {label}" \
--bapEnvId "{BAP_ENV_GUID}" \
--environmentType 200000000 \
[--environmentUrl "{environmentUrl}"]
Required args (per scripts/lib/create-deployment-environment.js):
--bapEnvId— the BAP environment GUID for the env being added. Resolve viapac env list(columnEnvironment ID) orpac env whofor the current source env. NOT the org/Dataverse URL.--environmentType—200000000for the dev/source env,200000001for each target env.--environmentUrlis optional and only echoed back into the output marker; it is not posted to Dataverse.
Capture stdout as JSON: const envResult = JSON.parse(output).
Store envResult.deploymentEnvironmentId as SOURCE_DEPLOYMENT_ENV_ID (for the dev source env) or append to TARGET_DEPLOYMENT_ENV_IDs (for each target). Also retain the bapEnvId value used for each call — Phase 5a's force-link auto-fix needs it if creation lands in a Failed state.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 859
- Forks
- 176
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
setup-pipeline- Source
- github.com/microsoft/power-platform-skills