name: architecture description: Architecture map, layer responsibilities, key files, auth types, and debug runbook by symptom. user-invocable: true
Architecture & Debug Guide
Use this skill to understand the codebase architecture, trace issues across repos, or debug failures. If given a specific error or symptom, use the architecture map below to identify which layer is failing and follow the targeted debug steps for that layer.
Repo Dependency Map
powerplatform-build-tools (THIS REPO — Azure DevOps Extension)
│
│ npm import (bundled)
▼
@microsoft/powerplatform-cli-wrapper (TypeScript action library)
│
│ child_process.spawn()
▼
pac CLI binary (pac.exe / pac — .NET, downloaded from NuGet at build time)
│
│ HTTPS REST
▼
Power Platform API (Dataverse / crm.dynamics.com)
Layer Responsibilities
| Layer | Owns | Does NOT own |
|---|---|---|
| build-tools | Azure DevOps task inputs, service connections, pipeline variables, artifact upload, task.json | Power Platform logic, pac CLI args |
| cli-wrapper | pac CLI argument assembly, auth flow, subprocess spawn/capture | Azure DevOps APIs, pipeline variables |
| pac CLI | Power Platform REST APIs, auth token management, all pac command semantics | Node.js, npm, Azure DevOps |
| Power Platform API | Dataverse operations, solution lifecycle | Everything above |
Full Call Chain (export-solution as example)
Azure DevOps Pipeline
│ task inputs: SolutionName, SolutionOutputFile, authenticationType, Environment
▼
src/tasks/export-solution/export-solution-v2/index.ts
├── TaskParser.getHostParameterEntries(taskDefinitionData)
│ └── reads task.json → Record<string, HostParameterEntry>
├── getCredentials()
│ ├── tl.getInput("authenticationType") → "PowerPlatformSPN" | "PowerPlatformEnvironment"
│ ├── tl.getEndpointAuthorization(endpointName)
│ └── returns AuthCredentials { appId, clientSecret, tenantId, cloudInstance }
├── getEnvironmentUrl()
│ └── checks (in order): task input → pipeline variable → env var → service connection URL
│
└── exportSolution(params, new BuildToolsRunnerParams(), new BuildToolsHost())
│ ↑ ↑
│ runnersDir from getInput() wraps
│ POWERPLATFORMTOOLS_ tl.getInput()
│ PACCLIPATH env var
▼
cli-wrapper: src/actions/exportSolution.ts
├── createPacRunner(runnerParams) → CommandRunner (spawn wrapper)
├── authenticateEnvironment(pac, credentials, environmentUrl)
│ └── pac("auth", "create", "--environment", url, "--applicationId", id,
│ "--clientSecret", base64secret, "--tenant", tenantId, "--cloud", "Public")
├── InputValidator.pushInput(args, "--name", params.name) → host.getInput(entry)
├── InputValidator.pushInput(args, "--path", params.path, resolveFolder)
├── InputValidator.pushInput(args, "--managed", params.managed)
│ ... (assembles full arg list)
│
├── pac("solution", "export", "--name", "MySolution", "--path", "/out/sol.zip", ...)
│ ▼
│ CommandRunner.ts: child_process.spawn("pac.exe", args, { env: { PP_TOOLS_AUTOMATION_AGENT: ... } })
│ ├── stdout lines → logger.log() → Azure DevOps build log
│ ├── stderr lines → logger.error() → Azure DevOps build log
│ └── exit 0 → resolve() | exit ≠ 0 → reject(RunnerError) → task fails
│
└── clearAuthentication(pac)
└── pac("auth", "clear")
Key Files by Layer
build-tools (this repo)
| File | Purpose |
|---|---|
src/tasks/<name>/<name>-v2/index.ts | Task entry point — reads inputs, calls cli-wrapper action |
src/tasks/<name>/<name>-v2/task.json | Declares all task inputs, metadata, and execution config — execution handler is Node20_1 (primary) + Node16 (fallback) |
src/host/BuildToolsHost.ts | Implements IHostAbstractions — wraps tl.getInput(), artifact store |
src/host/BuildToolsRunnerParams.ts | Implements RunnerParameters — pac path, logger, agent string |
src/host/CliLocator.ts | Finds pac.exe/pac binary path by platform (win32 vs linux) |
src/host/logger.ts | Implements cli-wrapper's Logger interface — routes to tl.warning/error/debug and console.log |
src/host/PipelineVariables.ts | Defines output pipeline variable names (BuildTools.EnvironmentUrl, BuildTools.EnvironmentId, etc.) — used by create-environment and others |
src/params/auth/getCredentials.ts | Extracts auth from service connection — handles PowerPlatformEnvironment (UsernamePassword), PowerPlatformSPN (SPN or WIF, detected by authorization.scheme) |
src/params/auth/getEnvironmentUrl.ts | Resolves target environment URL (4-level fallback chain) |
src/parser/TaskParser.ts | Converts task.json inputs array → Record<string, HostParameterEntry> |
extension/extension-manifest.json | Azure DevOps extension metadata |
extension/task-metadata.json | Task GUIDs per stage (LIVE / BETA / DEV / EXPERIMENTAL) |
extension/service-connections.json | Power Platform SPN endpoint definition |
nuget.json | pac CLI version pinned here — update to upgrade pac |
webpack.config.js | Finds all task index.ts files, compiles each to dist/tasks/*/index.js |
gulp/pack.mjs | Stages artifacts, injects task IDs per stage, runs tfx-cli to create VSIX |
cli-wrapper (inside node_modules/@microsoft/powerplatform-cli-wrapper)
| File | Purpose |
|---|---|
dist/actions/exportSolution.ts | Assembles pac args, calls pac, handles auth |
dist/actions/importSolution.ts | Same pattern for import |
dist/actions/*.ts | One file per pac command |
dist/pac/createPacRunner.ts | Resolves pac.exe path, returns CommandRunner |
dist/pac/auth/authenticate.ts | Builds pac auth create / pac auth clear calls |
dist/CommandRunner.ts | child_process.spawn() wrapper — stdout/stderr capture, exit code handling |
dist/host/InputValidator.ts | Pulls values through IHostAbstractions, normalizes, pushes to arg array |
Authentication Types & Environment Variable Map
| Auth Type | Service Connection Scheme | Credentials Passed to pac |
|---|---|---|
| Username/Password | PowerPlatformEnvironment | --username --password (base64) |
| Service Principal | PowerPlatformSPN | --applicationId --clientSecret (base64) --tenant |
| Workload Identity Federation | PowerPlatformSPN + WIF scheme | --applicationId --tenant --azureDevOpsFederated (via OIDC token env vars) |
Note: Managed Identity is NOT supported through PPBT.
getCredentials.tsonly handles the three types above. WIF is detected whenauthorization.scheme === "WorkloadIdentityFederation"inside thePowerPlatformSPNbranch — PPBT setsPAC_ADO_ID_TOKEN_REQUEST_URL/PAC_ADO_ID_TOKEN_REQUEST_TOKENenv vars and pac CLI handles the OIDC exchange.
Environment variables used by tasks:
| Variable | Set by | Used by |
|---|---|---|
POWERPLATFORMTOOLS_PACCLIPATH | tool-installer task | All other tasks (to find pac.exe) |
BuildTools.EnvironmentUrl | create-environment task | All other tasks (fallback env URL) |
agent.diagnostic | Azure DevOps agent | All tasks (enables --log-to-console on pac) |
PP_TOOLS_AUTOMATION_AGENT | CommandRunner.ts | pac CLI (user-agent telemetry) |
Bundled Dependency Notes (affects vulnerability fixes)
These packages are in bundleDependencies — they ship inside the VSIX and their
nested transitive deps are marked inBundle: true in package-lock.json.
npm overrides cannot reach them. Lock file must be patched directly.
| Bundled Package | Key nested dep | Known issue |
|---|---|---|
azure-pipelines-task-lib@5.2.8 | — | No known issues (upgraded from 4.x which had minimatch@3.0.5 ReDoS) |
@microsoft/powerplatform-cli-wrapper@0.1.135 | minimatch@10.x via glob@11 | Patched to 10.2.4 in lock file |
fs-extra | — | No known issues |
semver | — | No known issues |
Debug Runbook by Symptom
npm install hangs indefinitely
Cause: npm re-resolves the full dependency tree when a new uncached package version is introduced.
During re-resolution it hits npm.pkg.github.com for @microsoft/* packages.
- Check
~/.npmrchas a valid GitHub token://npm.pkg.github.com/:_authToken=<token> - Test auth:
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token <token>" https://npm.pkg.github.com/@microsoft%2fpowerplatform-cli-wrapper→ should return 200 - Run with verbose to find where it hangs:
npm install --verbose 2>&1 - Check npm debug log:
cat C:/.tools/.npm/_logs/<latest>-debug-0.log | tail -100 - If log shows 50,000+
placeDep ROOT minimatchlines → infinite loop from a conflicting flat minimatch override. Remove it.
npm install errors with registry auth
Cause: GitHub PAT expired or missing scopes.
- Token needs
read:packagesscope ongithub.com - Token goes in
~/.npmrcas//npm.pkg.github.com/:_authToken=<token> - The project
.npmrcsets@microsoft:registry=https://npm.pkg.github.com/andalways-auth=true
Task fails: "pac.exe not found" or "POWERPLATFORMTOOLS_PACCLIPATH not set"
Cause: tool-installer task did not run before this task, or failed silently.
- All tasks require
tool-installerto run first — it setsPOWERPLATFORMTOOLS_PACCLIPATH - Check
CliLocator.tsfor the exact path resolution logic - On Linux: pac binary needs execute permissions —
CliLocator.tssets them viachmod
Task fails: "auth create failed" or authentication errors
Layer: cli-wrapper authenticate.ts or pac CLI auth
- Check service connection credentials in Azure DevOps (Pipelines → Service Connections)
- Verify the auth type: SPN vs Username/Password vs Workload Identity
- Check
cloudInstance— wrong cloud (e.g. sending toPublicwhen env isUsGov) causes auth failure - Check
getCredentials.ts—resolveCloudInstance()maps the endpoint URL hostname to cloud name - Enable diagnostics: set
agent.diagnostic=truevariable in pipeline → adds--log-to-consoleto pac
Task fails: "Solution not found" or pac CLI errors
Layer: pac CLI → Power Platform API
- Enable
--log-to-console: setsystem.debug=truein pipeline variables - Check the exact pac command being run in the build log (each arg is logged)
- The error is from pac.exe — check Power Platform documentation for that specific pac command
- Verify environment URL:
getEnvironmentUrl.tshas a 4-level fallback — add explicitEnvironmentinput to the task to override
Build fails: TypeScript errors after upgrading packages
Layer: build-tools compile step
npm run build→ webpack + ts-loader- TypeScript target is ES5, module is CommonJS (
tsconfig.json) gulp-typescriptis on^6.0.0-alpha.1— type errors may surface after TS version upgrades- cli-wrapper interface changes break at compile time here — check
IHostAbstractionsandRunnerParameters
VSIX package has wrong task GUIDs / tasks conflict in marketplace
Layer: gulp/pack.mjs
- Task GUIDs per stage are in
extension/task-metadata.json - LIVE uses the canonical GUIDs; BETA/DEV/EXPERIMENTAL get unique GUIDs so they don't conflict
pack.mjssubstitutes GUIDs at pack time — verify the substitution logic if tasks show up under wrong IDs
Task fails: "Can't resolve AAD/OAuth authority"
Source: IcM recurring pattern (ADO #4863652) Layer: pac CLI auth → Power Platform API
- Usually caused by incorrect
cloudInstancevalue — checkgetCredentials.ts→resolveCloudInstance() - Verify the service connection URL matches the actual cloud (Public vs USGov vs China)
- Check if the tenant's AAD authority endpoint is reachable from the build agent (network/proxy issues)
- Enable diagnostics: set
agent.diagnostic=truein pipeline variables for verbose pac auth logging
Task fails: WhoAmI error in non-English build agent context
Source: ADO #4846644 (long-term, no code fix yet)
Cause: whoAmI.ts in cli-wrapper parses localized pac CLI output to extract the environment ID. Fails when the build agent OS locale is non-English.
Workaround: Ensure build agent uses English locale (en-US). Set LANG=en_US.UTF-8 on Linux agents.
Layer: cli-wrapper src/actions/whoAmI.ts ~line 40 — fix must go upstream to cli-wrapper repo.
Import solution fails with PVA (Power Virtual Agents) components
Source: IcM 604312672, ADO #4896777
Symptom: import-solution task fails when solution contains PVA components.
Layer: pac CLI → Power Platform API (not a build-tools code issue)
Workaround: Check pac CLI version — update nuget.json to latest pac CLI. If persists, raise with PAC CLI team.
Extension not working on Azure DevOps Server 2020 (on-prem)
Source: ADO #5130362 Official stance: Extension is not supported on Azure DevOps Server 2020 on-prem. Action: Deflect IcM tickets. Direct customers to Azure DevOps Services (cloud) or Server 2022+.
New pac CLI version needed
- Update version in
nuget.json - The
pacversion is also referenced inpackage.jsonfor documentation — keep in sync - Run
gulp restore(ornpm run prepare-pack) to download the new binary tobin/ - Test with
bin/pac/pac.exe --version
Vulnerability in a bundled dependency
Cause: Packages in bundleDependencies have inBundle: true in lock file. npm audit fix and overrides cannot touch them.
Fix procedure:
# 1. Get safe version metadata
npm view <package>@<safe-version> dist.tarball dist.integrity --json
# 2. Find the exact key in package-lock.json
# (may be nested: node_modules/foo/node_modules/bar)
node -e "const l=require('./package-lock.json'); console.log(Object.keys(l.packages).filter(k=>k.includes('<package>')))"
# 3. Patch the lock file entry: update version, resolved URL, integrity hash
# 4. Apply
npm install
npm audit # verify resolved
Task Pipeline Dependency Order
tool-installer ← MUST run first (sets POWERPLATFORMTOOLS_PACCLIPATH)
│
├── create-environment → sets BuildTools.EnvironmentUrl pipeline variable
│ │
│ ├── import-solution
│ ├── export-solution
│ ├── pack-solution / unpack-solution
│ ├── publish-customizations
│ ├── set-solution-version
│ └── deploy-package
│
├── backup-environment → restore-environment
├── copy-environment
└── delete-environment
Build Output Structure
npm run build
└── webpack → dist/tasks/<task-name>/<task-name>-v2/index.js (32 bundles)
npm run prepare-pack
├── webpack (above)
└── gulp restore → bin/pac/pac.exe + bin/pac_linux/pac
npm run pack
└── gulp pack
├── npm pack → out/npm-package/
├── stages to out/staging/ (dist/ + bin/ + extension/)
├── injects GUIDs from task-metadata.json (4 stages)
└── tfx-cli → out/packages/
├── PowerPlatform-BuildTools-<ver>-LIVE.vsix
├── PowerPlatform-BuildTools-<ver>-BETA.vsix
├── PowerPlatform-BuildTools-<ver>-DEV.vsix
└── PowerPlatform-BuildTools-<ver>-EXPERIMENTAL.vsix