microsoft/architecture
Architecture map, layer responsibilities, key files, auth types, and debug runbook by symptom.
npx skills add https://github.com/microsoft/powerplatform-build-tools --skill architecture
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.
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 | 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 |
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")
| 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 |
| 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 |
| 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.ts only handles the three types above. WIF is detected when authorization.scheme === "WorkloadIdentityFederation" inside the PowerPlatformSPN branch — PPBT sets PAC_ADO_ID_TOKEN_REQUEST_URL / PAC_ADO_ID_TOKEN_REQUEST_TOKEN env 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) |
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 |
|-----------------|---------------|-------------|
| [email protected] | — | No known issues (upgraded from 4.x which had [email protected] ReDoS) |
| @microsoft/[email protected] | [email protected] via glob@11 | Patched to 10.2.4 in lock file |
| fs-extra | — | No known issues |
| semver | — | No known issues |
npm install hangs indefinitelyCause: 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.
~/.npmrc has a valid GitHub token: //npm.pkg.github.com/:_authToken=<token>curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token <token>" https://npm.pkg.github.com/@microsoft%2fpowerplatform-cli-wrapper → should return 200npm install --verbose 2>&1cat C:/.tools/.npm/_logs/<latest>-debug-0.log | tail -100placeDep ROOT minimatch lines → infinite loop from a conflicting flat minimatch override. Remove it.npm install errors with registry authCause: GitHub PAT expired or missing scopes.
read:packages scope on github.com~/.npmrc as //npm.pkg.github.com/:_authToken=<token>.npmrc sets @microsoft:registry=https://npm.pkg.github.com/ and always-auth=trueCause: tool-installer task did not run before this task, or failed silently.
tool-installer to run first — it sets POWERPLATFORMTOOLS_PACCLIPATHCliLocator.ts for the exact path resolution logicCliLocator.ts sets them via chmodLayer: cli-wrapper authenticate.ts or pac CLI auth
cloudInstance — wrong cloud (e.g. sending to Public when env is UsGov) causes auth failuregetCredentials.ts — resolveCloudInstance() maps the endpoint URL hostname to cloud nameagent.diagnostic=true variable in pipeline → adds --log-to-console to pacLayer: pac CLI → Power Platform API
--log-to-console: set system.debug=true in pipeline variablesgetEnvironmentUrl.ts has a 4-level fallback — add explicit Environment input to the task to overrideLayer: build-tools compile step
npm run build → webpack + ts-loadertsconfig.json)gulp-typescript is on ^6.0.0-alpha.1 — type errors may surface after TS version upgradesIHostAbstractions and RunnerParametersLayer: gulp/pack.mjs
extension/task-metadata.jsonpack.mjs substitutes GUIDs at pack time — verify the substitution logic if tasks show up under wrong IDsSource: IcM recurring pattern (ADO #4863652)
Layer: pac CLI auth → Power Platform API
cloudInstance value — check getCredentials.ts → resolveCloudInstance()agent.diagnostic=true in pipeline variables for verbose pac auth loggingSource: 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.
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.
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+.
nuget.jsonpac version is also referenced in package.json for documentation — keep in syncgulp restore (or npm run prepare-pack) to download the new binary to bin/bin/pac/pac.exe --versionCause: 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
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
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
Take microsoft/architecture from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm.
Without those the skill loads but fails at the first command.