mcpField
E
ensure-pipelines-host

ensure-pipelines-host

github.com/microsoft云与 DevOps1.1k154 次安装今天更新
代码仓库

关于 ensure-pipelines-host

ensure-pipelines-host 是用于 Power Platform Pipelines 的主机检测与配置技能,供管理 Power Platform 部署环境的用户使用。它按 Power Apps 的解析顺序查找现有主机,并在需要时提供经确认的配置路径。

主要功能

  1. 01按解析顺序检测 Pipelines 主机
  2. 02识别 Platform Host 与 Custom Host
  3. 03支持复用现有主机
  4. 04提供经确认的主机配置路径
  5. 05缓存检查结果并验证可用性

适用场景

  1. 01为源环境查找已绑定的主机
  2. 02检测主机缺失时选择配置方式
  3. 03复核缓存主机是否仍可访问

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/microsoft/power-platform-skills --skill ensure-pipelines-host

安装后在 Claude Code 中输入/ensure-pipelines-host确认已加载。

附带文件scripts/validate-ensure-host.js

详细介绍

插件检查:运行 node "${PLUGIN_ROOT}/scripts/check-version.js" — 如果有消息输出,请先向用户显示,然后再继续。

ensure-pipelines-host

范围: 当源环境未绑定主机时,此技能会检测是否存在可复用的主机(Custom 或 PE);如果处于 NoHost 状态,则提供三种预配方式:新建 Platform Host(推荐;幂等,约 3–5 分钟);新建 Custom Host(仅限管理员,约 5–10 分钟);或通过 PPAC 手动预配(备用方案)。实现细节——端点名称、模板名称、BAP audience——见下文 Phase 4.0 / 4.A / 4.C;面向用户的说明则聚焦于结果。

Power Platform Pipelines 需要一个主机环境——一个安装了 Power Platform Pipelines 托管解决方案的 Dataverse 环境,管道、阶段、运行历史记录和项目都存放在其中。现有的 setup-pipeline 和 deploy-pipeline 技能假定主机已配置完成。本技能用于填补这一空缺。

我们了解的内容(事实来源)

本计划以三个主要来源为依据,优先级如下:

  1. useGetOrCreatePlatformEnvironment.v4.ts(Microsoft 内部客户端源代码 — power-platform-ux/packages/powerapps-appdeployment-ux/src/hooks/v4/)。定义了 Platform Environment 预配的准确 HTTP 契约:端点、请求正文、标头和轮询。
  2. ProjectHostProvider.tsx(同一仓库,src/components/ProjectHostProvider/)。定义了 Power Apps UI 用于确定哪个环境是源环境项目主机的确切解析顺序。我们会沿用该顺序,确保本技能与 UI 一致。
  3. eng.ms createcustompipelineshost(Microsoft 内部)。记录了 Custom Host 的快速路径:可通过标准环境创建 API 调用 D365_ProjectHost 组织模板,该模板会预装 Pipelines 应用。

公共 Microsoft Learn(learn.microsoft.com/power-platform/alm/{platform-host-pipelines, custom-host-pipelines, set-a-default-pipelines-host})以面向用户的方式描述了相同流程;我们引用它来说明用户熟悉的行为。PipelinesDeployScenario.har 和 Pipelines.har 中的 HAR 文件确认了读取端调用。

租户可能处于的三种主机状态

状态成因所在位置组织模板
Platform Host (PE)由 getOrCreate BAP 调用自动预配(或首次在 make.powerapps.com 中导航到 Pipelines 页面时作为副作用创建)。在环境选择器中隐藏。每个租户一个。位于租户主地理区域、由 Microsoft 管理的 Dataverse 环境D365_1stPartyAdminApps
Custom Host管理员通过 PPAC Deployments → New custom host 创建,或通过使用 D365_ProjectHost 模板的标准环境创建 API 创建,或在现有 Dataverse 环境上安装 Power Platform Pipelines 应用。租户中的常规 Dataverse 环境D365_ProjectHost(或在现有环境上安装应用)
源环境未绑定主机租户尚未在此环境中使用 Pipelines。——

当前的 discover-pipelines-host.js 只检查租户级的 DefaultCustomPipelinesHostEnvForTenant 设置。这只是多个信号之一。本技能会实现完整的解析顺序。

解析顺序(与 ProjectHostProvider.tsx 一致)

这是决定性的决策树,也是 Power Apps UI 的处理方式。我们会复现这一顺序,确保本技能与 UI 一致。

┌─────────────────────────────────────────────────────────────────────┐
│ 1. GetOrgDbOrgSetting('ProjectHostEnvironmentId') on source env     │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
            ┌──────────────┴───────────────┐
            │ value present                │ value empty
            ▼                              ▼
┌───────────────────────┐         ┌────────────────────────────┐
│ 2. Resolve env via    │         │ 5a. Tenant-wide search:    │
│    BAP GET            │         │   list envs + per-env      │
│    /environments/{id} │         │   /deploymentpipelines     │
└───────┬───────────────┘         │   probe.                   │
        │                         │                            │
   environmentSku?                │   - 1 Custom Host found →  │
        │                         │     AvailableUnboundCustom │
   ┌────┴────────────┐            │     (3.C-pre)              │
   │ Platform        │            │   - >1 Custom Hosts →      │
   │                 │            │     MultipleUnboundCustom  │
   │                 │            │     (3.C-pre')             │
   │                 │            │   - PE only →              │
   │                 │            │     PlatformHostExists-    │
   │                 │            │     Unbound (3.C-pre'')    │
   │                 │            │   - none → NoHost (3.C)    │
   │                 │            │                            │
   │                 │            │ 5b. Decision tree paths    │
   │                 │            │   for create-new (3.C):    │
   │                 │            │   - Platform getOrCreate   │
   │                 │            │     (fast-path, no admin)  │
   │                 │            │   - Custom D365_ProjectHost│
   │                 │            │     (fast-path, admin)     │
   │                 │            │   - Manual app install     │
   │                 │            │   - Manual PPAC create     │
   │                 │            └────────────────────────────┘
   ▼                 │
┌──────────────┐     │
│ 3. Check     │     │ environmentSku ≠ Platform (Custom Host)
│  Default-    │     ▼
│  Custom-     │   ┌──────────────────────────────┐
│  Pipelines-  │   │ 4. Use the Custom Host       │
│  HostEnv-    │   │    directly. Skip default-   │
│  ForTenant   │   │    custom check.             │
└──────┬───────┘   └──────────────────────────────┘
       │
   ┌───┴────────────────────────┐
   │ admin set a custom default │
   │                            │
   ▼                            ▼
┌─────────────────┐  ┌─────────────────────────┐
│ default ==      │  │ default !=              │
│ org setting?    │  │ org setting             │
│                 │  │                         │
│ → use default   │  │ → CannotRedirect ERROR  │
│   custom        │  │   (user locked to PE    │
└─────────────────┘  │   but admin overrode    │
                     │   at tenant scope)      │
                     └─────────────────────────┘

   if no admin default → use PE

来源:ProjectHostProvider.tsx 第 100–213 行(获取 orgSetting → 获取 defaultCustomPipelinesHost → 解析 finalProjectHostEnvironmentId)。

此技能不会做什么

以下是刻意排除的目标(每项都基于硬性约束或破坏性影响范围,详见下文的 设计约束):

  • 不会在不提示的情况下预配任何内容。 任何会创建环境或将源环境绑定到主机的操作,都必须先获得用户明确确认,并回显租户名称和租户 ID。PE 是租户单例且管理员不可删除,因此 Phase 4.0 的调用前确认关卡是防止为错误租户预配的主要缓解措施。getOrCreate 端点具有幂等性——如果在已有 PE 的租户上调用,它会返回现有 PE,而不是创建重复项。
  • 不会调用 Force Link 来将环境重新绑定到其他主机。Force Link 具有破坏性(创建者会失去对之前主机中现有管道的访问权限),并由单独的确认关卡控制,只有用户明确说“rebind”时才能触发。
  • 不会更改租户级别的 DefaultCustomPipelinesHostEnvForTenant 设置。 该设置近乎不可逆(之前默认主机中的现有管道将无法访问,详见 learn.microsoft.com/power-platform/alm/set-a-default-pipelines-host)。不在范围内。
  • 不会删除环境。
  • 不会直接写入 ProjectHostEnvironmentId。 绑定通过有文档说明的 Pipelines 流程建立(在主机中创建 deploymentenvironment 记录);直接写入组织设置会绕过验证。

身份验证策略:优先使用 PAC,BAP 作为回退(--source auto)

读取侧检测(Phase 2 的解析顺序、环境列表、按 ID 查找环境)默认使用 --source auto:

  1. 如果提供了 BAP 令牌,先尝试使用 BAP 获取环境列表或读取环境(数据更丰富,包含 lastModifiedTime、permissions、tenantId)。
  2. 遇到 HTTP 401 或 403 时,通过 pac-bap-shim.js 回退到 pac admin list --json。PAC 在 BAP 上有自己的第一方客户端 ID 授权,Az CLI 不一定会继承这些授权(已于 2026-04-28 验证:即使受众声明正确,演示租户 D365DemoTSCE53051106 仍会拒绝用于 BAP 的 Az 令牌)。
  3. 如果完全没有提供 BAP 令牌,则直接使用 PAC。

PAC shim 会返回 BAP 格式的数据;下游代码(SKU 筛选、排序、分类)保持不变。PAC 不提供的字段(tenantId、lastModifiedTime、permissions、isManaged)会返回 null,这些字段对主机检测都不是关键字段。PAC 也不会显示 Platform Host(PE),因为 pac admin list 不包含 Platform SKU 环境;检测 PE 需要使用 --source bap 并提供有效的 BAP 令牌。

写入侧操作(provision-custom-host.js 中的环境创建 POST 请求、生命周期操作轮询)仍需要 BAP。即使环境列表调用失败,具有正确受众的 Az CLI 令牌通常仍可用于这些操作,因为 BAP RP 对操作和读取实施了不同的策略。如果 provision-custom-host.js 返回 401,用户必须在目标租户中注册服务主体(或使用 PPAC UI 回退路径 4.C)。

设计约束

  1. 选择 PE 时必须进行 JIT 预配——无论 PE 是现有的还是刚刚预配的。 来自 ProjectHostProvider.tsx(第 232–240 行)的注释:“在 Platform Environment 场景中,用户可能尚未在其中完成预配,因此 BAP 无法发现它。因此,我们将使用通过 getOrCreate 调用检索到的组织 URL 来发起此首个请求,以便触发用户 JIT。” 当第 2 阶段检测到现有 PE 且用户接受它(第 3.A 阶段)时,或者当第 4.0 阶段通过 getOrCreate 预配新的 PE 时,第 5 阶段针对 instanceApiUrl 发起的 WhoAmI 调用会在任何后续主机操作之前触发 JIT。(对于 Custom Host 路径,调用方按设计已拥有访问权限。)
  2. CannotRedirect 是真实的终止状态,并非理论上的边缘情况。当 ProjectHostEnvironmentId(源环境中的组织设置)指向 PE,而 DefaultCustomPipelinesHostEnvForTenant(管理员租户设置)指向其他位置时,就会发生这种情况。技能必须检测到此情况并将其作为特定错误呈现——若静默跳过,就会将管道操作路由到错误的主机。
  3. 仅限管理员的 Custom Host 快速路径。 PPAC 的 New custom host 流程受 DeploymentHubCreatePipelinesHostForAdminsOnly 限制,仅向 Global / Power Platform / Dynamics 管理员显示(eng.ms 文档)。BAP 环境创建 API 也需要相应的权限。非管理员会收到 403;该技能会在预检中提示用户,并优雅地回退到手动路径。
  4. BAP 环境 GET 返回的 404 含义不明确。 对于已删除、已禁用、没有 PE 和无访问权限,都会返回 404,且不会区分(PowerPipelines_PE_Knowledge.md §6.A)。我们绝不会将单个 404 视为“不存在主机”——采取操作前,我们会通过 list-environments 和组织设置进行交叉验证。
  5. 每个环境一次只能绑定一个主机。 重新绑定需要使用 Force Link,这会对先前的主机造成破坏性影响。超出范围(见非目标)。
  6. 该技能在用户 OAuth 上下文中运行——使用与 Power Apps UI 相同的范围和受众。BAP 调用使用 https://service.powerapps.com/ 受众。

getOrCreate 的幂等性——BAP getOrCreate 端点具有幂等性(现有 PE 返回 200 + provisioningState === 'Succeeded';新 PE 返回 202 + 生命周期操作)。第 4.0 阶段会利用这一点——对已拥有 PE 的租户调用 getOrCreate 是安全的,只会返回现有 PE。provision-platform-host.js 帮助程序通过返回值中的 alreadyExisted: true | false 标志体现这一差异(记录在 docs/alm/last-host-check.json 遥测块中,字段为 platformHostAlreadyExisted)。

先决条件

  • PAC CLI 已登录(pac env who 执行成功)
  • Azure CLI 已登录(az account show 执行成功)
  • 源 Dataverse 环境 URL(如果从 Power Pages 项目中调用,则从 powerpages.config.json 读取;否则作为参数传入)
  • 对于第 4 阶段的管理员专属路径:调用方具有 Global / Power Platform / Dynamics 管理员权限(如果缺少权限,技能会检测到并清晰地呈现 403)

阶段

阶段 1 — 检测前置条件并收集租户上下文

在本阶段开始时预先创建所有任务。

要创建的任务:

  1. “检查本地缓存并检测前置条件”
  2. “按解析顺序查找主机”
  3. “向用户确认操作”
  4. “执行所选路径”
  5. “即时预配并验证主机”
  6. “写入主机检查产物”

步骤:

  1. 本地缓存快速路径。 如果 docs/alm/last-host-check.json 存在,且 Date.now() - Date.parse(checkedAt) < cacheMaxAgeMs(默认 24h;可通过 --cacheMaxAgeHours 配置):

    • 为缓存的 finalHostEnvUrl 源获取 HOST_TOKEN。
    • 执行一次低成本探测:GET {finalHostEnvUrl}/api/data/v9.0/solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$select=version&$top=1(一次往返即可证明 Pipelines 已安装并获取版本)
      • 200 → 缓存有效。根据缓存文件设置 RESOLUTION。设置 ACTION_TAKEN = "none"。跳过阶段 2–5;跳转到阶段 6,并附上“已复用缓存的主机”摘要。
      • 404 / 403 / 超时 / 网络错误 → 缓存已过期或不再可访问。继续执行步骤 1(完整解析)。不要失败——环境删除或权限变更后缓存过期是预期情况。
    • 如果文件缺失、格式错误、早于 cacheMaxAgeMs,或包含 ready: false → 继续执行步骤 1。
    • 如果传入 --no-cache,则完全跳过此步骤(用于 CI / 冒烟测试)。
  2. 运行 verify-alm-prerequisites.js:

    node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js"
    

    获取 .envUrl(devEnvUrl)、.token(DEV_TOKEN)、.userId、.tenantId、.organizationId。如果身份验证失败,使用脚本的修复提示停止。

  3. 运行 detect-project-context.js(非致命错误——此技能在 Power Pages 项目之外也有效):

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

    获取 .siteName 和 .solutionManifest,用于消息提示。

  4. 获取 BAP 令牌(受众与 Dataverse 不同):

    az account get-access-token --resource "https://service.powerapps.com/" --query accessToken -o tsv
    

    保存为 BAP_TOKEN。阶段 2 和阶段 4 中的所有 BAP /providers/Microsoft.BusinessAppPlatform/... 调用都会使用此令牌。

3a. 解析租户显示名称(单次尝试,尽力而为)。阶段 1.4 和阶段 4.0 会同时显示易读的租户名称和租户 GUID,以便用户验证目标租户。从 Microsoft Graph 组织终结点获取:

az rest --method GET --url "https://graph.microsoft.com/v1.0/organization?$select=id,displayName" --resource "https://graph.microsoft.com/" --query "value[0].displayName" -o tsv

保存为 TENANT_DISPLAY_NAME。若发生任何失败(无 Graph 权限、网络错误、多租户歧义),则回退为 TENANT_DISPLAY_NAME = null 并继续——阶段 1.4 / 4.0 提示会在显示名称为 null 时仅显示租户 GUID。

🚦 关卡(同意 · ensure-pipelines-host:1.4.tenant-identity): 在检测任何主机之前,显示租户显示名称、租户 GUID 和开发环境 URL。这是第一道错误租户防护。取消后会正常退出,不会进行任何 BAP/Dataverse 调用。

  1. 租户身份确认关卡。 通过 AskUserQuestion 显示以下信息:

    “即将检查租户 {TENANT_DISPLAY_NAME}({tenantId})、组织 {organizationId}、开发环境 {devEnvUrl} 的 Pipelines 主机配置。是否继续?1. 是 / 2. 取消”

    (如果 TENANT_DISPLAY_NAME 为 null,则省略加粗的租户名称部分,并以租户 GUID 开头。)

    这是用于防止对错误租户执行操作的同意关卡中的第一道。

阶段 1.5——以当前 Pipelines 主机文档为依据

参考:${PLUGIN_ROOT}/references/alm-docs-grounding.md

此步骤最多用时约 30 秒。如果 MCP 搜索 / 获取出错,记录一行说明并继续——此技能必须支持离线运行。

  1. 使用查询 Power Platform Pipelines host environment Platform Host Custom Host 运行 microsoft_docs_search。
  2. 通过 microsoft_docs_fetch 并行获取 https://learn.microsoft.com/en-us/power-platform/alm/pipelines(以及最多一个关于主机设置、默认自定义主机配置或管理员角色要求的相关页面)。
  3. 提取一段摘要,说明 Microsoft Learn 当前对 Platform Host 与 Custom Host 权衡、解析顺序(组织数据库设置 → BAP 环境元数据 → 租户默认值)以及管理员角色要求的说明。与此技能自身的 Resolution order 部分和 ${PLUGIN_ROOT}/references/cicd-pipeline-patterns.md 进行比较;标记任何差异(例如新增 Platform-Host SKU、默认自定义主机设置名称变更、新的租户策略控件)。
  4. 使用该摘要为阶段 2 及后续阶段的决策提供依据。不要悄悄更改技能行为——在阶段 3(向用户确认操作)之前,将任何差异作为提示告知用户。
阶段 2——按解析顺序查找主机

此阶段为只读操作。它会生成一个 RESOLUTION 对象,供用户确认阶段进行分支判断。

此阶段与 ProjectHostProvider.tsx 完全一致。该钩子中的 useState 变量映射到 RESOLUTION 中的字段:

TS 变量我们的字段
orgSetting.orgDbOrgSettingValueorgSettingHostEnvId
initialProjectHostEnvironmentId(相同)
isInitialHostPlatformEnvironmentisPlatform
defaultCustomPipelinesHosttenantDefaultCustomHostEnvId
finalProjectHostEnvironmentIdfinalHostEnvId
projectHostStatusstatus

步骤:

  1. 组织设置探测(对应 tsx 第 103 行的 useGetOrgDbOrgSetting('ProjectHostEnvironmentId'))。新建辅助程序 check-env-host-binding.js:

    POST {devEnvUrl}/api/data/v9.0/GetOrgDbOrgSetting
    Authorization: Bearer {DEV_TOKEN}
    Body: { "SettingName": "ProjectHostEnvironmentId" }
    
    • SettingValue 为空 → 当前没有绑定。跳至步骤 4。
    • 不为空 → 保存为 orgSettingHostEnvId。继续步骤 2。
  2. 通过 BAP 解析环境(对应 tsx 第 483 行的 useGetEnvironmentByName(initialProjectHostEnvironmentId))。新建辅助程序 resolve-env-by-id.js:

    GET https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments/{envId}?api-version=2020-06-01&$expand=properties.linkedEnvironmentMetadata,properties.permissions
    Authorization: Bearer {BAP_TOKEN}
    
    • 200 → 获取 environmentSku、displayName、linkedEnvironmentMetadata.instanceApiUrl、linkedEnvironmentMetadata.instanceUrl。设置 RESOLUTION.isPlatform = (environmentSku === 'Platform')。
    • 404 → 先消除歧义再采取行动(约束 5)。运行 list-tenant-envs.js(步骤 5),并检查环境是否在列表中:
      • 如果在列表中 → 用户无权访问 → 设置 RESOLUTION.status = "PermissionDenied",告知用户并停止。
      • 如果不在列表中 → 环境确实已删除 / 已禁用 → 设置 RESOLUTION.status = "OrgSettingStale",建议用户清除 ProjectHostEnvironmentId 并重新运行,然后停止。
    • 403 → 设置 RESOLUTION.status = "PermissionDenied",然后停止。
  3. 如果 isPlatform === true,则对应默认自定义租户设置检查(tsx 第 148–213 行)。复用现有的 discover-pipelines-host.js:

    node "${PLUGIN_ROOT}/scripts/lib/discover-pipelines-host.js" \
      --envUrl "{devEnvUrl}" --token "{DEV_TOKEN}" --userId "{userId}"
    
    • found: false → 租户没有管理员默认自定义主机。finalHostEnvId = orgSettingHostEnvId(PE)。设置 RESOLUTION.status = "AvailableUsingPlatformHost"。
    • found: true 且 hostEnvUrl 与 orgSettingHostEnvId 匹配 → 管理员默认值与组织设置一致。finalHostEnvId = orgSettingHostEnvId。设置 RESOLUTION.status = "AvailableUsingCustomHostByAdminDefault"。
    • found: true 且 hostEnvUrl 与 orgSettingHostEnvId 不匹配 → CannotRedirect(约束 3)。设置 RESOLUTION.status = "CannotRedirect",记录两个 URL。停止并显示具体错误消息——只有管理员才能解决此问题。

    如果 isPlatform === false(Custom Host):直接使用。finalHostEnvId = orgSettingHostEnvId。设置 RESOLUTION.status = "AvailableUsingCustomHost"。跳过步骤 4–5,转至步骤 6。

  4. 未配置组织设置 → 在宣布为 NoHost 前进行租户范围搜索。 源环境未绑定,但租户中可能已有可用的自定义主机(由管理员创建,或由本技能在其他项目中的先前运行创建)。提供创建选项前,始终先盘点。

  5. 租户环境盘点 + Pipelines 存在性探测(用于决策 — 为 RESOLUTION.status 提供依据)。新增辅助工具 list-tenant-envs.js:

    步骤 5a — 列出环境:

    GET https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments?api-version=2020-06-01&$expand=properties.linkedEnvironmentMetadata
    Authorization: Bearer {BAP_TOKEN}
    

    对每个环境,记录 { envId, displayName, environmentSku, instanceApiUrl, isManaged, hasDataverse: !!instanceApiUrl }。

    步骤 5b — 探测每个环境是否存在 Pipelines(并行,最多 10 个并发;受 SKU 筛选器和 maxEnvsToProbe 上限约束):

    预筛选(避免在大型租户中探测每个环境 — 侦察发现有些租户拥有 1000 多个环境):

    • 跳过没有 Dataverse 的环境(linkedEnvironmentMetadata.instanceApiUrl == null)。
    • 跳过不在 --skus 中的环境(默认值:Production,Sandbox — 两者均可通过 Phase 4.B 在现有环境上安装,作为 Pipelines 应用的主机)。PE 始终报告 environmentSku === 'Platform',因此无论如何都会纳入。传入 --skus Production,Sandbox,Trial 可纳入 Trial 环境(可通过 4.B 安装应用,但不能通过 4.A 创建环境 — 拥有 Trial 许可证的租户在创建环境时会收到 NotEnoughCapacity_HasTrialLicense)。
    • 按 lastModifiedTime 降序排序。
    • 限制为 --maxEnvsToProbe(默认值为 50;使用 10 并发,可在 5 秒内覆盖典型租户的 80% 情况)。
    • 如果达到上限仍未找到主机,则显示警告:"已扫描 N 个环境,共 M 个(筛选条件:Production+Sandbox,按 lastModifiedTime 排序)。传入 --maxEnvsToProbe N+ 或 --skus Production,Sandbox,Trial 以扩大范围。"

    探测查询(单次查询同时完成存在性检查和版本捕获):

    GET {instanceApiUrl}/api/data/v9.0/solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$select=uniquename,version&$top=1
    Authorization: Bearer {HOST_TOKEN-per-env}
    OData-Version: 4.0
    OData-MaxVersion: 4.0
    
    • 200 且 value.length === 1 → 已安装 Pipelines。记录 value[0].version 作为 pipelinesSolutionVersion。标记为自定义主机候选项(如果 environmentSku === 'Platform',则标记为 PE)。
    • 200 且 value.length === 0 → 未安装 Pipelines。如果有 Dataverse 且调用方有访问权限,则标记为 eligible-for-app-install。
    • 404 → 实体存在,但 Dataverse 无法访问 / URL 错误;将其视为非候选项。
    • 403 → 调用方无访问权限;不要将其计为主机候选项。仅为警告添加到 inaccessibleEnvs[]。
    • 超时 / 5xx → 记录到警告中,并将其视为非候选项;不要重试。

    为什么不使用 deploymentpipelines?$top=0? 即使主机已正确安装,Dataverse 也会对 deploymentpipelines 上的 $top=0 返回 HTTP 400“Invalid value for $top query option”(已于 2026-04-28 在 pascalepipelineshost.crm.dynamics.com 上验证)。solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor' 查询才是正确且低开销的探测方式 — 只需一次往返;使用 $top=1 无需担心速率限制,而且还会返回我们需要的版本。

    步骤 5b 的令牌策略:针对每个不同的环境源,获取一个 HOST_TOKEN,使用 az account get-access-token --resource "{origin}",并在本次运行中缓存在内存里。除非资源不存在(环境已删除),否则令牌获取本身不应失败;若资源不存在,则跳过。

    步骤 5 的输出(RESOLUTION.candidates):

    {
      existingCustomHosts: [{ envId, instanceApiUrl, displayName, environmentSku, pipelinesSolutionVersion }, ...],
      existingPlatformHost: { envId, instanceApiUrl, displayName, ... } | null,  // at most one
      eligibleForAppInstall: [{ envId, instanceApiUrl, displayName }, ...],
      inaccessibleEnvs: [{ envId, displayName, reason: "403" | "timeout" }, ...]
    }
    

    判定逻辑(设置 RESOLUTION.status):

    • existingCustomHosts.length === 1 → RESOLUTION.status = "AvailableUnboundCustomHost"。暂时将 finalHostEnvId / finalHostEnvUrl 设为该主机(Phase 3.C-pre 将确认)。
    • existingCustomHosts.length > 1 → RESOLUTION.status = "MultipleUnboundCustomHosts"。Phase 3.C-pre' 将询问用户选择。
    • existingCustomHosts.length === 0 AND existingPlatformHost !== null → RESOLUTION.status = "PlatformHostExistsUnbound"。Phase 3.C-pre'' 提供使用 PE 的选项(无需创建;租户中已有 PE)。注意:源环境实际绑定到 PE,是通过文档所述的 Pipelines 流程完成的,即 setup-pipeline 在 deploymentenvironments 中注册源环境,与 Custom Host 的方式相同。
    • 全部为零 → RESOLUTION.status = "NoHost"。进入 Phase 3.C 决策树(新建)。

    **自行检测说明:**此技能之前的运行创建的 Custom Host(Phase 4.A D365_ProjectHost 模板)会在模板中安装 Pipelines 解决方案。它们会以与管理员创建的主机完全相同的信号出现在 existingCustomHosts 中。我们不需要为自行创建的主机添加标记——检测到已安装 Pipelines 解决方案的信号就足够了。

  6. 探测 Pipelines 解决方案版本(仅当 finalHostEnvId 已知且步骤 5b 尚未填充该值时)。在已解析主机的 instanceUrl 上:

    GET {hostEnvUrl}/api/data/v9.0/solutions?$filter=uniquename eq '{PIPELINES_SOLUTION_UNIQUE_NAME}'&$select=version,installedon
    Authorization: Bearer {HOST_TOKEN}
    

    其中,HOST_TOKEN 是通过 az account get-access-token --resource 针对 {hostEnvUrl origin} 获取的。

    该解决方案的确切唯一名称尚待确定——请参阅 待解决事项。当前假设:msdyn_AppDeploymentAnchor。记录 PIPELINES_SOLUTION_VERSION。如果查询结果为空(非 PE 主机上缺少解决方案)→ RESOLUTION.status = "HostWithoutPipelines"——进入 Phase 3.D 路径。

向用户报告结果:

"租户 {tenantId} 的主机状态:{RESOLUTION.status}。{与状态对应的摘要行。}"

Phase 3 — 与用户确认操作

根据 RESOLUTION.status 分支。每个分支最终都会进入 "proceed to Phase 5"(主机已可用)或 "Phase 4 with chosen path"。

3.A — 状态 AvailableUsingCustomHost / AvailableUsingCustomHostByAdminDefault / AvailableUsingPlatformHost

主机已就绪。确认后跳过后续步骤。

"找到现有主机:{finalHostEnvUrl}({RESOLUTION.status},Pipelines 解决方案版本 v{PIPELINES_SOLUTION_VERSION})。使用此主机?

  1. 是——继续验证
  2. 取消"
  • 是 → 设置 ACTION_TAKEN = "none",跳转到 Phase 5。
  • 取消 → 退出。
3.B — 状态 CannotRedirect

状态已锁定。无法继续。

"无法继续:{devEnvUrl} 上的 ProjectHostEnvironmentId 指向 Platform Host({orgSettingHostEnvId}),但租户管理员将 DefaultCustomPipelinesHostEnvForTenant 设为了其他环境({tenantDefaultCustomHostEnvId})。Pipelines UI 无法将此环境重定向到管理员选择的环境。需要 Power Platform 管理员执行以下任一操作才能解决:(a) 清除租户默认值;或 (b) 更新此环境上的组织设置。正在退出。"

停止。

3.C-pre — 状态 AvailableUnboundCustomHost(找到一个现有自定义主机)

租户中已恰好有一个安装了 Pipelines 的自定义主机。源环境尚未绑定到该主机,但在 setup-pipeline 将源环境注册到主机的 deploymentenvironments 表中时,会自动完成绑定。复用可避免创建重复主机。

“在租户 {tenantId} 中找到一个现有自定义主机:

  • 显示名称: {displayName}
  • URL: {instanceApiUrl}
  • Pipelines 解决方案: v{pipelinesSolutionVersion}

源环境 {devEnvUrl} 尚未绑定到该主机——首次针对该主机运行 setup-pipeline 时会自动完成绑定。使用此主机吗?

  1. 是 — 使用现有主机(推荐;可避免重复)
  2. 否 — 显示创建新主机的决策树(阶段 3.C)
  3. 取消”
  • 是 → 设置 finalHostEnvUrl/Id,ACTION_TAKEN = "reuse-existing-custom",跳转到阶段 5。
  • 否 → 转到阶段 3.C 的 NoHost 决策树(仍允许创建另一个主机)。
  • 取消 → 退出。
3.C-pre' — 状态 MultipleUnboundCustomHosts(找到多个现有自定义主机)

“在租户 {tenantId} 中找到 {N} 个已安装 Pipelines 的现有自定义主机。请选择一个使用,或创建新主机:

  1. {host[0].displayName}({host[0].instanceApiUrl},Pipelines v{host[0].pipelinesSolutionVersion})
  2. {host[1].displayName}(...) ... N. ... N+1. 改为创建新的自定义主机 — 转到阶段 3.C 决策树 N+2. 取消”
  • 选择 1..N → 根据所选主机设置 finalHostEnvUrl/Id,ACTION_TAKEN = "reuse-existing-custom",跳转到阶段 5。
  • N+1 → 转到阶段 3.C 的 NoHost 决策树。
  • N+2 → 退出。
3.C-pre'' — 状态 PlatformHostExistsUnbound(PE 已存在,没有自定义主机)

租户中已存在一个 PE(任何人首次导航到 Pipelines 页面时,系统会自动预配一个)。根据范围决策,此次迭代不会自动预配 PE;但如果 PE 已存在,则会提供使用选项。

“租户 {tenantId} 已有一个平台主机({instanceApiUrl},Pipelines v{pipelinesSolutionVersion})。源环境尚未绑定到该主机。使用此主机吗?

  1. 是 — 使用现有平台主机(幂等 — 已在此租户中预配)
  2. 否 — 改为创建自定义主机(阶段 3.C 决策树)
  3. 取消”
  • 是 → 根据 PE 设置 finalHostEnvUrl/Id,ACTION_TAKEN = "reuse-existing-pe",跳转到阶段 5。(阶段 5 的 WhoAmI 调用会触发 JIT — 约束 1。)
  • 否 → 转到阶段 3.C 的 NoHost 决策树(出于治理考虑,优先使用管理员创建的自定义主机)。
  • 取消 → 退出。
3.C — 状态 NoHost(主机类型决策树)

🚦 关卡(计划 · ensure-pipelines-host:3.C.host-type): 顶层主机类型提示 — 选择平台主机 / 自定义主机 / PPAC 手动操作 / 手动导出-导入策略 / 取消。决定阶段 3 和阶段 4 后续流程的走向。

触发条件: 状态为 NoHost,且上游没有 hostResolution.willProvision* 标记提供答案。 询问原因: 自动选择会在未经同意的情况下预配环境(PE 或自定义主机);PE 在租户中为单例,且管理员无法删除。 取消后的状态: 无 — 尚未触发任何预配。

🚦 关卡(计划 · ensure-pipelines-host:3.C.env-pick): 阶段 3.C 顶层选项“自定义主机”下的子提示 — 显示符合条件的环境列表(最多 5 个),附带角色标签(dev env、source env、staging env、production env),并提供“其他(粘贴 URL)”选项。仅在主机类型菜单中的自定义主机分支下触发。

触发条件: 用户在阶段 3.C 顶层选择“自定义主机”。 询问原因: 自动选择错误的环境会导致通过用户无意使用的主机运行 Pipelines。 取消后的状态: 无 — 尚未触发应用安装。 提示会先要求用户选择主机类型(Platform Host、Custom Host、PPAC 手动操作或取消)。选择 Custom Host 后,会显示子提示,要求选择安装方式(使用现有环境或新建环境)。Platform-Host 路径是阻力最小的默认选项,并排在首位。

跳过规则——调用方已收集答案。 从 setup-pipeline 调用此技能时,如果 docs/alm/last-pipeline.json 包含由 plan-alm Phase 2 Q4 填充的 hostResolution 块,请先检查这些标志,再显示提示:

上游信号操作
hostResolution.willProvisionPlatform === true完全跳过 Phase 3.C。直接转到 Phase 4.0(预配新的 Platform Host)。4.0 中的调用前确认门仍会执行——请参阅下文 4.0“调用前确认(不可跳过)”。
hostResolution.chosenEnvUrl 是非空 URL完全跳过 Phase 3.C。设置 CHOSEN_ENV_URL = hostResolution.chosenEnvUrl,并携带该环境直接转到 Phase 4.B(适用 4.B 步骤 1 中“已选择”的路径)。根据现有路由表设置 ACTION_TAKEN(如果来源与 devEnvUrl 匹配,则设为 "user-installed-app-on-dev",否则设为 "user-installed-app")。
hostResolution.willProvisionCustom === true 且 chosenEnvUrl 为空跳过 Phase 3.C。直接转到 Phase 4.A(预配新的 Custom Host)。4.A 中的调用前确认门仍会执行。
hostResolution.willUsePpac === true 且 chosenEnvUrl 为空跳过 Phase 3.C。直接转到 Phase 4.C(PPAC 手动操作)。
以上均不符合按下文所述执行 Phase 3.C。

此跳过规则的原因。 plan-alm Phase 2 Q4 的 NoHost 分支会显示相同的主机类型菜单,让用户在规划时、查看已呈现的 ALM 计划时就做出选择。执行时在 3.C 再次询问,会迫使用户对同一问题回答两次,还可能导致 Agent 将其中一个答案视为权威答案而忽略另一个(这是 2026-05-05 出现的 trial-license-409 → wrong-env-fallback 链式问题的根源)。只要存在上游信号,就应予以信任。

步骤 1:显示顶层主机类型提示。

“没有与 {devEnvUrl} 绑定的 Pipelines 主机。应使用哪个环境托管 Pipelines?

每个租户的 Pipelines 都位于一个环境中;管道、阶段和运行历史记录均存储在该环境中。源环境通过它进行部署。

  1. 预配 Platform Host(推荐) — 由 Microsoft 管理的 Dataverse 环境,会在租户的主地理区域中自动预配。已预装 Pipelines 应用。幂等(可安全重复运行)。约 3–5 分钟。

  2. 设置 Custom Host — Pipelines 位于你控制的 Dataverse 环境中。我们会询问你是要使用现有环境,还是创建一个全新的专用环境。

  3. 打开 PPAC 并手动创建 — 如果选项 2 不适用,可使用此备用方案。

  4. 取消 — 退出。”

顶层路由:

选择操作
选项 1Phase 4.0(包含调用前确认门)。ACTION_TAKEN = "fast-path-platform-getorcreate"。
选项 2显示下方步骤 2 中的 Custom-Host 子提示。
选项 3Phase 4.C。ACTION_TAKEN = "user-created-custom-ppac"。
选项 4退出。

步骤 2:构建符合条件的环境列表并应用角色标签(仅当用户选择选项 2 → 子选项 a 时需要;仅在选择选项 2 后再按需执行)。

从 Phase 2 获取 RESOLUTION.candidates.eligibleForAppInstall[](具有 Dataverse、调用方有权访问、尚未安装 Pipelines、sku ∈ 默认筛选条件 {Production, Sandbox} 的环境;对于试用许可证租户,通过 --skus 将范围扩展为 Production,Sandbox,Trial)。

在展示列表时,为每个条目添加项目上下文标签。按 URL 源匹配(转换为小写、去掉末尾斜杠、忽略路径和查询参数)。多个标签之间用 · 连接。

标签来源信号
dev env技能正在使用的环境 URL(来自调用方的 devEnvUrl / pac env who)
source envdocs/alm/last-pipeline.json 中的 sourceEnvironmentUrl(通常与 dev env 相同)
staging envdocs/alm/last-pipeline.json 中任一阶段的 targetEnvironmentUrl,且阶段名称匹配 /stag|test|uat/i
production env阶段名称匹配 /prod/i 的任一阶段的 targetEnvironmentUrl

没有匹配角色的环境不显示标签。

步骤 2a:对符合条件的环境列表排序并限制数量。 超过约 7–8 个选项时,AskUserQuestion 将无法使用。采用最多展示 5 个环境的限制,并按角色进行排序:

  1. 始终显示带角色标签的环境(最高优先级):包括步骤 2 中添加了 dev env、source env、staging env 或 production env 标签的任何符合条件的环境。按 URL 来源去重。
  2. 从其余符合条件的列表中补足剩余名额,最多 5 个,顺序沿用 list-tenant-envs.js 的原生排序(名称提示模式 → 管理员权限 → lastModifiedTime)。
  3. 始终追加一项“Other (paste URL)”作为最后一项。

单独跟踪符合条件的环境总数——当 eligible.length > 5 时,在提示中显示数量差值。

步骤 2b:空列表时的处理。 如果筛选后符合条件的列表为空,先尝试扩大 SKU 筛选范围,将 Trial 也包含在内(使用 --skus Production,Sandbox,Trial 重新调用 ensure-pipelines-host-detect.js)。如果仍为空,从 Custom-Host 子提示中移除子选项 a——仅显示 b(新建)和 c(返回)。打印 SKU 筛选详情,以便用户覆盖筛选条件:

"没有环境符合 SKU 筛选条件(Production、Sandbox、Trial)。运行 --skus <comma-list> 以进一步扩大范围,或选择 b 创建全新环境,或选择 c 返回。"

步骤 2c:显示 Custom-Host 子提示(用户选择选项 2 时):

"你希望如何设置 Custom Host?

a. 使用现有环境——在其中安装 Pipelines 应用。{eligibleCountSuffix} 从符合条件的环境中选择:

  • {env[0].displayName}({env[0].instanceApiUrl})— {environmentSku} — {如有标签则显示}
  • {env[1].displayName}(...)
  • …(最多 5 项)
  • Other (paste URL) — 可用于任何未列在此简表中的符合条件的环境

⚠ Sandbox SKU 环境在安装前会触发确认提示。

b. 创建专用全新环境——使用模板 D365_ProjectHost 自动创建环境。预装 Pipelines 应用。需要 Global / Power Platform / Dynamics 管理员权限。约需 5–10 分钟。

c. 返回——回到顶层主机类型菜单。"

{eligibleCountSuffix} 替换规则:

  • eligible.length <= 5 → 空字符串(不添加后缀;显示所有环境)。
  • eligible.length > 5 → 显示前 5 个,共 {N} 个;其余 {N-5} 个符合条件的环境可通过 "Other (paste URL)" 选项访问。(开头有空格)

当用户选择“Other (paste URL)”时,预填充 URL 输入框,内容为以下命令获取的环境列表:node "${PLUGIN_ROOT}/scripts/lib/list-environments.js"(将 pac env list 解析为 JSON { displayName, environmentId, environmentUrl, uniqueName, active };当前 PAC CLI 不支持旧命令 pac env list --output json),以便用户从清单中复制粘贴或选择,而不必手动输入 URL。

更改此提示时要验证的测试场景:

  • 符合条件的环境为 0 个 → 移除子选项 a(子提示仅显示 b / c)。
  • 符合条件的环境为 1–5 个 → 在提示中列出全部,不添加后缀。
  • 符合条件的环境为 6 个或更多,且存在带角色标签的环境(dev/staging/prod)→ 首先显示所有带角色标签的环境;按排序补足其余名额;后缀显示数量差值。
  • 符合条件的环境为 6 个或更多,且没有带角色标签的环境 → 按排序显示前 5 个;后缀显示数量差值。

步骤 3:根据子提示的回答进行路由。

SelectionPhase 4 路径ACTION_TAKEN
Option 1(顶层 Platform Host)4.0(调用前确认,然后运行 provision-platform-host.js)"fast-path-platform-getorcreate"
Option 2 → 子选项 a — 从列表中选择环境,URL 与 devEnvUrl 匹配(源相同)4.B(跳过“选择哪个环境”子提示;将所选环境 URL 作为 CHOSEN_ENV_URL 传入)"user-installed-app-on-dev"
Option 2 → 子选项 a — 选择列表中的其他环境4.B(跳过子提示;传入 CHOSEN_ENV_URL)"user-installed-app"
Option 2 → 子选项 a — “Other (paste URL)”4.B,将用户提供的 URL 作为 CHOSEN_ENV_URL"user-installed-app"
Option 2 → 子选项 a — 选择的环境满足 environmentSku === "Sandbox"Step 4 Sandbox 确认关卡,确认后进入 4.B(确认前暂不设置)
Option 2 → 子选项 b4.A(子提示:名称、区域、管理员确认)"fast-path-custom-d365projecthost"
Option 2 → 子选项 c(返回)重新显示顶层菜单n/a
Option 34.C"user-created-custom-ppac"
Option 4退出n/a

对于 Option 1 和子选项 b,触发调用前会经过不可跳过的调用前确认关卡,向用户显示租户身份。Option 1 → 参见 Phase 4.0“调用前确认”。子选项 b → 参见 Phase 4.A“调用前确认(不可跳过)”。

Step 4(条件性):Sandbox 确认关卡。

如果在子选项 a 中选择的环境满足 environmentSku === "Sandbox",则显示:

"⚠ {displayName} 是一个 Sandbox 环境(environmentSku: Sandbox)。

文档说明,Power Platform Pipelines 在 Production 环境中运行。Sandbox 应该也能正常运行,但不在受支持的环境矩阵中。

是否继续?

  1. 是,继续并自行承担风险
  2. 选择其他环境
  3. 取消"
  • 是 → 使用 Sandbox 环境继续执行 4.B。根据上述开发环境匹配规则设置 ACTION_TAKEN。
  • 选择其他环境 → 重新显示子选项 a 的环境列表(保留选取器的其余状态)。
  • 取消 → 返回顶层 Phase 3.C 提示。

遥测说明。 将 "user-installed-app" 和 "user-installed-app-on-dev" 分开后,我们便能在推出后了解用户将 Pipelines 与开发环境放在一起,还是使用独立环境。新的 "fast-path-platform-getorcreate" 值可以帮助我们衡量用户对全新、摩擦最低路径的采用情况;工件中的 telemetry.platformHostAlreadyExisted 标志可区分幂等的已存在结果(200)和新预配结果(202)——如果我们发现 Phase 4.0 经常返回 200,这将有助于调整 Phase 2.5 的 --maxEnvsToProbe 默认值。

3.D — 状态 HostWithoutPipelines(罕见)

Host 环境已存在,但缺少 Pipelines 解决方案。

"找到 Host 环境 {finalHostEnvUrl},但尚未安装 Pipelines 解决方案。是否立即通过 PPAC 安装?

  1. 是 — 打开 PPAC 并安装(手动引导)
  2. 否 — 退出"
  • 是 → 使用预选环境进入 Phase 4.C。
  • 否 → 退出。
3.E — 状态 OrgSettingStale / PermissionDenied

向用户显示具体故障。此问题不在自动修复范围内。建议手动清理。

Phase 4 — 执行所选路径
4.0 — 快速路径:通过 getOrCreate 创建平台主机

🚦 关卡(确认 · ensure-pipelines-host:4.0.pre-call): 调用 BAP getOrCreate 平台主机接口之前,先在请求正文中回显租户身份。即使上游 hostResolution.willProvisionPlatform === true,也不可跳过。PE 是每个租户唯一且租户管理员无法删除的环境——此关卡是防止操作错误租户的主要措施。

触发条件: 进入阶段 4.0(从 3.C 或上游路由而来)。 询问原因: PE 创建在了错误的租户中;租户管理员无法删除。 取消后的状态: 无——尚未开始创建。

这是创建主机所需操作最少的路径。使用 D365_1stPartyAdminApps + Platform 请求正文调用幂等的 BAP getOrCreate 接口。与用户在 make.powerapps.com → Pipelines 页面点击“Get started”时触发的调用相同——我们直接调用即可。规范见 useGetOrCreatePlatformEnvironment.v4.ts。新建的辅助程序为 provision-platform-host.js。

无子提示词。 BAP 会选择租户的主区域和默认显示名称;无需管理员角色。

通过 AskUserQuestion 进行调用前确认——不可跳过的单一确认关卡:

“即将为租户 {TENANT_DISPLAY_NAME}({tenantId})创建平台主机。

平台主机是 Microsoft 在租户主区域中托管的 Dataverse 环境。每个租户只有一个,且操作幂等——如果已有平台主机,将返回现有主机。创建新的平台主机大约需要 3–5 分钟。

是否继续?1. 是 / 2. 取消”

(当 TENANT_DISPLAY_NAME 为 null 时,省略加粗的租户名称部分,直接显示租户 GUID。)

即使上游 hostResolution.willProvisionPlatform === true,也仍需运行此关卡的原因——与 4.A 的调用前关卡理由相同:此处会回显确切的租户身份,这是用户发现操作租户错误的最后机会。PE 是每个租户唯一且租户管理员无法删除的环境;此关卡是主要防范措施。实现细节(BAP 接口、请求正文格式)有意不放在面向用户的提示中,而是保存在此 SKILL.md 和 provision-platform-host.js 源码中。

调用:

node "${PLUGIN_ROOT}/scripts/lib/provision-platform-host.js" \
  --bapToken "{BAP_TOKEN}" \
  --correlationId "{uuid v4}"

响应处理(由辅助程序负责,但路由决策在此处进行):

  • 200 + properties.provisioningState === 'Succeeded' → 租户已有 PE(幂等路径)。辅助程序返回 { status: 'Succeeded', alreadyExisted: true, instanceApiUrl, ... }。设置 RESOLUTION.finalHostEnvUrl、finalHostEnvId、RESOLUTION.isPlatform = true、actionTaken = "fast-path-platform-getorcreate"、telemetry.platformHostAlreadyExisted = true。继续进入阶段 5。*防御性说明:*这表示阶段 2.5 的枚举未发现 PE(可能是因为 --maxEnvsToProbe 达到上限后停止,尚未查询到 PE,也可能是阶段 2 和 4.0 之间发生了竞态)。记录一条调试级说明;不要报错。
  • 202 + Location → 辅助程序轮询,直到 provisioningState === 'Succeeded',然后返回 { status: 'Succeeded', alreadyExisted: false, instanceApiUrl, ... }。设置相同字段,并将 telemetry.platformHostAlreadyExisted 设为 false。
  • 403 → 辅助程序抛出异常,错误内容原样保留 BAP 响应正文;建议运行 az logout && az login。提供切换到选项 2/3/4 的备用方案。(不要复用 4.A 中要求管理员权限的说明——getOrCreate 不要求管理员权限。)
  • 其他 4xx / 5xx → 辅助程序抛出异常;显示错误,并询问用户是重试还是切换路径。

成功后: 设置 RESOLUTION.isPlatform = true(这样阶段 5 会采用 JIT 分支——约束 1)。继续进入阶段 5。

4.A — 快速路径:通过 D365_ProjectHost 模板创建自定义主机

🚦 门禁(同意 · ensure-pipelines-host:4.A.pre-call): 在发起 Custom Host 创建调用前,回显 BAP 环境创建请求正文(区域 + 显示名称 + 模板)。即使上游 hostResolution.willProvisionCustom === true,也不得跳过。此为发现错误租户或错误区域预配的最后机会。2026-05-05 修复的 Bug 正是因跳过此门禁导致的。

触发条件: 进入阶段 4.A;已收集子提示词(显示名称、区域、管理员确认)。 询问原因: Custom Host 可能在错误的租户或区域中预配;会消耗 Azure 容量配额;还可能将环境归属到错误的组织。 取消后状态: 无事发生——尚未启动预配。

🚦 门禁(同意 · ensure-pipelines-host:4.sandbox-confirm): 如果选定的安装目标环境具有 environmentSku=Sandbox,则显示 Sandbox 限制警告,并要求在安装前明确再次确认。每次选中 Sandbox SKU 环境时触发一次。

触发条件: 阶段 4.B / 阶段 3.C 子选项 a——选定的环境为 Sandbox SKU。 询问原因: 未经同意就在 Sandbox 环境中安装 Pipelines——Sandbox 环境可能会因不活跃而被删除,且容量较低;Pipelines 可能会意外中断。 取消后状态: 无事发生——尚未启动应用安装。

使用 D365_ProjectHost 模板的标准环境创建 API(eng.ms-documented;PPAC 的 New custom host 内部使用相同模板)。新增辅助程序 provision-custom-host.js。

通过 AskUserQuestion 收集 API 调用前的子提示:

  1. 显示名称(默认建议:"{tenant displayName} Pipelines Host")
  2. 区域(默认使用 BAP tenant 端点提供的租户主区域;可选择覆盖)
  3. 确认调用者是管理员——单个 AskUserQuestion:"您是此租户中的全局管理员 / Power Platform 管理员 / Dynamics 管理员吗?是 / 否 / 不确定"。如果选择“否”或“不确定”,建议使用路径 4.B/4.C 并回退。

通过 AskUserQuestion 进行调用前确认——不可跳过的第二道同意门禁:

"即将为租户 {tenantId} 调用 POST https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments?api-version=2021-04-01,请求正文如下:

{
  "location": "{region}",
  "properties": {
    "displayName": "{display name}",
    "environmentSku": "Production",
    "databaseType": "CommonDataService",
    "linkedEnvironmentMetadata": { "templates": ["D365_ProjectHost"] }
  }
}

将预配一个已预装 Pipelines 应用的 Custom Host(约需 5–10 分钟)。是否继续?1. 是 / 2. 取消"

此门禁必须始终执行。 不得因为 plan-alm Q4 已收到“继续使用 PP Pipelines”的确认,或因为上游 hostResolution.willProvisionCustom === true,或因为用户刚刚在管理员确认中回答“是”,就跳过此门禁。plan-alm Q4 和管理员确认针对的是策略;此门禁则会回显确切的 API 调用正文(URL、区域、租户、模板),是用户发现错误租户或错误区域预配的最后机会。2026-05-05 修复的 Bug 正是因为 Agent 将 plan-alm 预确认视为已涵盖此门禁而出现的。

调用:

POST https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments?api-version=2021-04-01
Authorization: Bearer {BAP_TOKEN}
Content-Type: application/json
x-ms-correlation-id: {uuid v4}

{
  "location": "{region}",
  "properties": {
    "displayName": "{display name}",
    "environmentSku": "Production",
    "databaseType": "CommonDataService",
    "linkedEnvironmentMetadata": { "templates": ["D365_ProjectHost"] }
  }
}

响应处理:

  • 202 + Location 标头 + Retry-After 标头 → 轮询 Location URL,直到生命周期操作完成。
  • 200 + 立即返回的正文(环境创建时很少见)→ 捕获 URL。
  • 初始 POST 返回 403 → 停止,并提示 “Custom Host 快速通道需要 Global / Power Platform / Dynamics 管理员权限。如果你在 Dataverse 环境中拥有 system-admin 权限,建议使用路径 2(在现有环境中安装 Pipelines 应用);如果你可以请求管理员协助,建议使用路径 3(PPAC UI)。” 无缝回退到 4.B / 4.C。
  • 409 且错误代码与容量相关(例如 NotEnoughCapacity_HasTrialLicense_ProvisionEnvironment、NotEnoughCapacity、NotEnoughCapacity_OrganizationDisabled、EnvironmentCapacityExceeded)→ 回退到路径 4.B 前,先提示用户选择回退 SKU。许可证 / 容量限制通常仅适用于所请求的 SKU;较小的 SKU(Sandbox、Developer、Trial)通常可在同一租户中成功创建,并生成功能完全相同的 Pipelines 主机(Pipelines 应用可安装在任何 SKU 上,区别仅在许可证分配)。请参阅下面的子步骤“4.A — SKU 回退提示”。只有在用户拒绝回退 SKU,或回退尝试也失败后,才重新进入阶段 3.C / 路径 4.B。
  • 其他 4xx / 5xx → 显示错误,并询问用户是重试还是切换路径。切换到 4.B 时,遵循上文相同的“使用原始符合条件的环境列表”规则。

轮询:

GET {Location}
Authorization: Bearer {BAP_TOKEN}

间隔 = Retry-After 秒数(默认 10s)。超时时间 = 15min(可配置)。收到每个响应时,读取 provisioningState(和/或操作的 state 字段——执行期间确认):

  • Creating / InProgress → 继续轮询
  • Succeeded → 完成;捕获 instanceApiUrl、name(环境 GUID)、displayName
  • Failed / Canceled → 显示错误并停止

**成功后:**设置 RESOLUTION.finalHostEnvUrl、finalHostEnvId、instanceApiUrl、actionTaken = "fast-path-custom-d365projecthost"。继续阶段 5。

4.A — SKU 回退提示词(容量错误补救)

当 env-create 返回与容量相关的 409 错误时,用户的租户没有足够的可用许可证/容量来满足所请求的 SKU,但可能有足够的容量来使用较小的 SKU。请清楚说明此限制,并在建议采用 Path 4.B 之前提供可选的回退 SKU:

  1. 读取错误正文。提取:

    • error.code(例如 NotEnoughCapacity_HasTrialLicense_ProvisionEnvironment)
    • error.message(BAP 提供的面向用户的说明,例如 "Trial licenses are limited to creating Trial environments only.")
  2. 构建回退 SKU 列表。默认顺序为 Production(推荐)→ Sandbox → Developer → Trial,并移除刚刚失败的 SKU。每个 SKU 都有不同的注意事项:

    SKU建议使用的情况需要说明的注意事项
    Production默认首选无 — 文档中指定的 Pipelines 主机 SKU
    Sandbox租户有订阅,但没有多余的 Production 容量"Sandbox SKU 可用于 Pipelines,但文档中将其标记为非生产环境。Microsoft 可能会对托管在 Sandbox 上的应用采用不同的 SLA。"
    Developer个人开发者租户"Developer SKU 为单用户 SKU。其他团队成员无法通过此主机进行部署。仅当这是个人/仅供开发使用的 ALM 设置时才使用。"
    Trial试用许可证租户(没有其他可行选项时)"Trial 环境将在 30 天后过期,除非进行转换。到期时需要重新预配 Pipelines 主机。"
  3. 告知用户此限制并显示提示:

    "自定义主机预配失败,错误代码为 {error.code}:{error.message}

    Pipelines 应用在任何 SKU 上的安装方式都相同 — 只有许可证分配不同。你可以使用较小的 SKU 重试 env-create,也可以改为在现有 env 上安装 Pipelines 应用(Path 4.B)。"

    🚦 关卡(计划 · ensure-pipelines-host:4.A.sku-fallback): env-create 遇到容量错误 — 使用其他 SKU 重试、回退到 Path 4.B(在现有 env 上安装),或取消。

    AskUserQuestion(动态构建选项列表 — 移除刚刚失败的 SKU,并在说明中附上注意事项):

    问题标题选项
    使用不同的 SKU 重试 env-create?SKU 回退(每个剩余 SKU 对应一个选项,并在说明中注明注意事项)、回退到 Path 4.B(在现有 env 上安装 Pipelines 应用)、取消
  4. 根据回答进行分支处理:

    • 选择了 SKU:使用新 SKU 替换正文中的原 SKU,再次发出 4.A 调用前确认关卡(不可跳过 — 请参见上文“调用前确认”),然后使用 --environmentSku <picked> 再次调用 provision-custom-host.js。如果仍然遇到容量错误,则再次显示提示,并移除下一个 SKU。连续两次遇到容量错误后,停止提供 SKU 回退选项,并转到 Path 4.B。
    • Path 4.B:根据下文的“原始符合条件的 env 列表”规则转到 4.B。
    • 取消:正常退出。

    始终丢弃 409 响应正文中返回的任何 env GUID — 预配失败,因此该 GUID 要么不代表可用的 env,要么只是附带产生的结果。Path 4.B 必须仅使用 Phase 2 符合条件的 env 清单。

4.B — 在现有 env 上安装 Pipelines 应用(自动化)

此前,此路径需要手动点击进入 PPAC。截至 2026-05-08,此流程已完全自动化:install-pipelines-app.js 会调用 BAP applicationPackages/install endpoint(与 PPAC“安装应用”按钮背后的 API 相同);当 BAP 返回 401/403/5xx 时,则会回退到 pac application install。

子提示:选择目标 env(已选择 env 时跳过):

  1. 如果环境已被选定(已设置 CHOSEN_ENV_URL,无论是来自阶段 3.C 选项 2 → 子选项 a,还是来自针对 hostResolution.chosenEnvUrl 的上游跳过规则):跳过子提示 — 直接带着所选环境进入安装步骤。按阶段 3.C 步骤 3 的路由表设置 ACTION_TAKEN(当 CHOSEN_ENV_URL 的来源与 devEnvUrl 匹配时设为 "user-installed-app-on-dev",否则设为 "user-installed-app")。

    否则(旧入口路径 — 调用方未经过 3.C 直接调用了 4.B,或者经由 4.A 的失败回退到达此处):展示子提示 “哪个环境将托管 Pipelines?(来自阶段 2 清单的自动检测环境):”,选项为来自 RESOLUTION.candidates.eligibleForAppInstall[] 的符合应用安装条件列表,外加“其他(粘贴 URL)”。当从 4.A 的 409 试用许可证回退到达时,符合条件列表仍然适用 — 丢弃 409 响应体中出现的任何环境 GUID。

环境 GUID 健全性检查。 传给安装助手的 GUID 必须来自阶段 2 中枚举的所选环境的 name 字段(或者对于用户粘贴的 URL,来自 pac env list)。切勿替换为来自 4.A 失败响应、来自不同运行批次的 docs/alm/last-host-check.json,或来自任何其他状态路径的 GUID。如果你无法有把握地确定 GUID,请在调用助手之前,通过 AskUserQuestion 展示环境显示名称 + URL + GUID,请用户确认。

调用前确认门(不可跳过 — 与阶段 4.0 / 4.A 相同的理由):

“即将在租户 {TENANT_DISPLAY_NAME}({tenantId})的 {displayName}({instanceApiUrl})上安装 Power Platform Pipelines 应用。这与 PPAC 的 Install app 按钮执行的是同一次安装 — Agent 直接调用 BAP API,因此无需手动点击。耗时约 2–5 分钟。是否继续? 1. 是 / 2. 取消”

调用助手:

node "${PLUGIN_ROOT}/scripts/lib/install-pipelines-app.js" \
  --bapToken "{BAP_TOKEN}" \
  --envId "{envId}" \
  --instanceApiUrl "{instanceApiUrl}" \
  --hostToken "{HOST_TOKEN}" \
  --correlationId "{uuid}"

HOST_TOKEN 是针对所选环境的来源获取的(az account get-access-token --resource "{instanceApiUrl origin}")并传递进去,以便助手的安装后验证探测(solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor')能够端到端运行,而无需该技能串联单独的验证步骤。

响应处理(委托给助手,但路由决策在此处):

  • { status: 'Succeeded', alreadyInstalled: true, installPath: 'cached' } — 该包已安装在此环境上(幂等路径;在 4.B 中罕见,因为阶段 2.5 本应已将该环境归类为主机,但这是防御性处理)。按上述 dev-env-match 规则设置 ACTION_TAKEN,并继续到阶段 5。
  • { status: 'Succeeded', alreadyInstalled: false, installPath: 'bap' } — BAP 的 applicationPackages/install POST 成功(200 同步或 202 + Location 轮询)。按上述 dev-env-match 规则设置 ACTION_TAKEN,并继续到阶段 5。
  • { status: 'Succeeded', alreadyInstalled: false, installPath: 'pac', pacFallbackReason: '...' } — BAP 返回 401/403/5xx(通常是租户中 Az → BAP 被拒绝时的令牌受众不匹配,与 pac-bap-shim.js 为环境枚举覆盖的场景相同);助手回退到 pac application install。结果相同,在 docs/alm/last-host-check.json 遥测中记录回退原因。继续到阶段 5。
  • 助手抛出异常 — BAP 和 PAC 均失败。向用户展示合并后的错误消息。最后手段回退(手动):打印 PPAC URL https://admin.powerplatform.microsoft.com/manage/environments/{envId}/dynamics365apps 以及四个手动步骤(Install app → Power Platform Pipelines → Next → 接受条款 → Install),并附带一个后续的 “已完成 — 继续” AskUserQuestion。确认后,针对该环境 URL 运行 verify-host-readiness.js,仅当检测到 Pipelines 解决方案时才继续到阶段 5。此分支应很少出现;如果频繁触发,请使用助手的合并错误提交 issue,以便我们调整 BAP/PAC 路径。 成功时: 记录 helper 的 pipelinesSolutionVersion(在传入 instanceApiUrl + hostToken 时填充)。设置 RESOLUTION.finalHostEnvUrl/Id。继续执行第 5 阶段。
4.C — 引导式手动操作:PPAC New custom host

🚦 关卡(进度 · ensure-pipelines-host:4.C.ppac-done): 外部系统等待关卡——用户必须手动完成 PPAC“New custom host”流程,然后返回确认。之后,Skill 会轮询 BAP,查找新环境。

触发条件: 进入第 4.C 阶段;已打印 PPAC URL。 要求确认的原因: Skill 会在未确认新环境是否存在的情况下继续执行;随后 setup-pipeline 会因没有主机而失败。更糟的是,如果 PPAC 创建流程进行到一半时用户取消了关卡,主机绑定就会处于部分建立状态(租户中会存在一个以 D365_ProjectHost 为模板的环境)。 取消后的状态: host-binding——租户中可能会留下手动创建的环境;用户必须通过 PPAC 清理。

  1. 打印:https://admin.powerplatform.microsoft.com/deployments 和以下说明:“单击‘New custom host’ → 填写名称(建议:‘{tenant} Pipelines Host’)→ 选择租户主区域中的 Production 环境 → 创建。预配需要 5–10 分钟。”

    根据 eng.ms 文档:“面板默认会选择 Production 环境类型。还必须添加 Dataverse……此处会隐藏模板和示例应用选项,因为此场景使用特定的组织模板。”(该模板为 D365_ProjectHost,与路径 4.A 自动执行时使用的模板相同。)

  2. 使用双选项 AskUserQuestion:“已完成——已启动预配” / “取消”。

  3. 用户确认后,每隔 15 秒轮询一次 BAP list-tenant-envs.js,查找带有 Pipelines 标记的新环境。检测到后,记录 URL,设置 actionTaken = "user-created-custom-ppac"。继续执行第 5 阶段。

通用:超时处理

每条路径默认超时为 15 分钟(可配置)。超时后,询问用户要延长(再延长 15 分钟)、切换路径还是退出。

第 5 阶段 — JIT 预配(仅限检测到 PE 时)并验证主机

无论 finalHostEnvUrl 是如何获取的,始终运行。

JIT 步骤(仅当检测并选择了现有 PE 时运行——RESOLUTION.isPlatform === true): 根据约束 2,调用用户可能很久以前就在 PE 中完成了 JIT 预配,也可能从未完成。为了确保交接前主机上的身份验证正常,我们会针对 instanceApiUrl 发出一次 WhoAmI 请求。(Custom Host 路径也需要执行相同步骤,但下方 verify-host-readiness.js 的第 1 步会自然满足此要求——创建环境的管理员必然拥有访问权限;对于 user-installed-app,用户已拥有环境访问权限。)

GET {instanceApiUrl}/api/data/v9.0/WhoAmI
Authorization: Bearer {HOST_TOKEN}

其中,HOST_TOKEN = az account get-access-token --resource "{instanceApiUrl origin}"。

预期结果:返回 200 和 UserId。如果首次调用返回 404 / 403,则每隔 5 秒重试一次,最多 60 秒——JIT 传播有时是异步的。

验证(verify-host-readiness.js)——按顺序检查:

  1. WhoAmI 返回 UserId(证明身份验证正常,并在检测到 PE 的情况下触发 JIT)。
  2. GET {hostEnvUrl}/api/data/v9.0/solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$select=version&$top=1 返回一行 → 记录 PIPELINES_SOLUTION_VERSION。(一次查询即可同时检查是否已安装 Pipelines 并记录版本;Dataverse 会以 400 拒绝 deploymentpipelines?$top=0。)

与 MIN_PIPELINES_VERSION(scripts/lib/alm-thresholds.js 中的常量)进行比较。

  • 所有检查均通过 → READY = true。
  • 解决方案版本低于最低要求 → 发出警告(非致命)。
  • 任意检查失败 → 停止并提供针对该项检查的修复方法。
第 6 阶段 — 写入主机检查产物

写入 docs/alm/last-host-check.json(如果 docs/alm/ 目录不存在,请先创建 — node -e "require('fs').mkdirSync('docs/alm',{recursive:true})";或者在项目外调用时使用 --outputPath):

{
  "schemaVersion": 2,
  "checkedAt": "2026-04-28T...",
  "tenantId": "...",
  "sourceEnvUrl": "{devEnvUrl}",
  "sourceEnvId": "...",
  "resolutionStatus": "AvailableUsingPlatformHost" | "AvailableUsingCustomHost" | "AvailableUsingCustomHostByAdminDefault" | "AvailableUnboundCustomHost" | "MultipleUnboundCustomHosts" | "PlatformHostExistsUnbound" | "CannotRedirect" | "NoHost" | "OrgSettingStale" | "PermissionDenied" | "HostWithoutPipelines",
  "finalHostEnvUrl": "...",
  "finalHostEnvId": "...",
  "finalHostInstanceApiUrl": "...",
  "isPlatformHost": true | false,
  "tenantDefaultCustomHostEnvId": "...",
  "actionTaken": "none" | "reuse-existing-custom" | "reuse-existing-pe" | "fast-path-platform-getorcreate" | "fast-path-custom-d365projecthost" | "user-installed-app" | "user-installed-app-on-dev" | "user-created-custom-ppac",
  "pipelinesSolutionVersion": "9.x.y.z",
  "ready": true,
  "warnings": [
    "Pipelines solution version 9.0.0.1 is below recommended 9.1.0.0 — RetrieveDeploymentPipelineInfo may not be available."
  ],
  "candidates": {
    "existingCustomHosts": [
      { "envId": "...", "instanceApiUrl": "...", "displayName": "...", "pipelinesSolutionVersion": "..." }
    ],
    "existingPlatformHost": null,
    "eligibleForAppInstall": [
      { "envId": "...", "instanceApiUrl": "...", "displayName": "..." }
    ],
    "inaccessibleEnvs": [
      { "envId": "...", "displayName": "...", "reason": "403" }
    ]
  },
  "telemetry": {
    "correlationId": "{uuid passed to env-create, if applicable}",
    "platformHostAlreadyExisted": true | false
  }
}

telemetry.platformHostAlreadyExisted — 仅当 actionTaken === "fast-path-platform-getorcreate" 时存在。当 BAP getOrCreate 返回 200 + Succeeded 时为 true(幂等 — 租户已有 PE);当调用返回 202 并且我们轮询至完成时为 false。此字段可用于衡量第 2.5 阶段的枚举漏掉现有 PE 的频率,从而调整 --maxEnvsToProbe 的默认值。

架构版本升级(1 → 2): 新增 candidates.* 块,用于记录租户范围内的枚举结果。缓存快速路径(第 1 阶段步骤 0)无论 schemaVersion 为何都会读取 finalHostEnvUrl;candidates 块仅供参考,有助于调试/重新运行决策。旧版 v1 文件仍可读取 — 缺失的字段均视为“尚未枚举”。

actionTaken 枚举值定义:

  • "none" — 本次运行前已确定主机;未执行安装/预配(第 3.A 阶段路径或缓存快速路径)。
  • "reuse-existing-custom" — 用户选择了租户中已有的未绑定 Custom Host(第 3.C-pre 或 3.C-pre')。
  • "reuse-existing-pe" — 用户选择了现有的 Platform Host(第 3.C-pre'')。
  • "fast-path-platform-getorcreate" — 第 4.0 阶段调用了 BAP getOrCreate 端点。telemetry.platformHostAlreadyExisted 标志可区分幂等复用现有主机(200)与新预配主机(202)。
  • "fast-path-custom-d365projecthost" — 第 4.A 阶段通过环境创建 API 预配了新的 Custom Host。
  • "user-installed-app" — 第 4.B 阶段在现有环境中安装了 Pipelines,且该环境不是开发环境。
  • "user-installed-app-on-dev" — 第 4.B 阶段在开发环境本身安装了 Pipelines(URL 源与 devEnvUrl 匹配)。与 "user-installed-app" 的遥测数据分开记录,以便了解用户将 Pipelines 与开发环境共置的频率。
  • "user-created-custom-ppac" — 第 4.C 阶段 — 用户通过 PPAC UI 创建了环境;流程在创建后检测到了该环境。

此文件由 setup-pipeline 和 deploy-pipeline 使用。

记录技能使用情况:

参考:${PLUGIN_ROOT}/references/skill-tracking-reference.md

按照参考中的技能跟踪说明记录此技能的使用情况。使用 --skillName "EnsurePipelinesHost"。

刷新 ALM 计划(如果存在):

node "${PLUGIN_ROOT}/scripts/lib/refresh-alm-plan-data.js" \
  --projectRoot "." \
  --phase ensure-pipelines-host \
  --render

这会根据刚写入的 docs/alm/last-host-check.json 更新 planData.hostResolution(仅主机 — 尚无管道),并移除运行前的 NoHost 风险,然后重新渲染 docs/alm-plan.html。请在此处执行,而不只是放在 setup-pipeline 第 7 阶段 — 主机安装可能耗时 18 分钟以上并跨越会话边界,因此延迟刷新可能导致计划始终无法反映主机。当 docs/.alm-plan-data.json 不存在时(独立运行,不属于 ALM 计划),该辅助程序会返回 ok:false,并作为软性无操作处理。集中式 PostToolUse 钩子也会作为后备机制协调计划,但在源头刷新可立即确保渲染后的计划保持最新。

展示摘要表:

字段值
租户{tenantId}
源环境{devEnvUrl}
解析状态{resolutionStatus}
最终主机{finalHostEnvUrl}
主机类型Platform / Custom
执行的操作{actionTaken}
Pipelines 版本{pipelinesSolutionVersion}
警告{warnings}

如果 actionTaken !== "none":

"下一步: 运行 /power-pages:setup-pipeline,以此主机创建第一个管道。"

与现有技能集成

setup-pipeline(✅ 已接入)

setup-pipeline/SKILL.md Phase 1 第 4 步调用 ensure-pipelines-host-detect.js(编排器包装程序),并根据 resolutionStatus 分支:

  • AvailableUsing* → 直接使用 finalHostEnvUrl,继续执行。
  • *Unbound* / NoHost → 委派给 /power-pages:ensure-pipelines-host(此技能)进行复用或预配;当 docs/alm/last-host-check.json 显示 ready: true 后恢复执行。
  • CannotRedirect / OrgSettingStale / PermissionDenied → 停止,并显示相应的管理员处理消息。

Phase 3 中原有的“手动向用户询问主机 URL”后备方案已移除 — HOST_ENV_URL 始终由 Phase 1 填充,或者技能会在进入 Phase 3 前停止。

deploy-pipeline

无变化。deploy-pipeline 从 setup-pipeline 写入的 docs/alm/last-pipeline.json 中读取 hostEnvUrl。

plan-alm(✅ 已接入)

plan-alm Phase 1 第 12 步调用 ensure-pipelines-host-detect.js,并将结果存储为 HOST_RESOLUTION(当 PIPELINE_DONE = true 时跳过)。Phase 2 Q4 根据 HOST_RESOLUTION.status 分支。生成的 docs/alm-plan.html 包含一张“Pipelines 主机”卡片,并且(当 willEnsureDuringExecution: true 时)在“设置管道”检查清单步骤下添加一个子项。请参阅 references/cicd-pipeline-patterns.md 和 plan-alm-update-PLAN.md 规范。

威胁模型 — 内置缓解措施

风险此技能中的缓解措施
混淆代理 / 静默预配Phase 1.4 租户身份验证关卡 + Phase 3 明确选择 + Phase 4.A 调用前确认,回显确切的请求正文
重复创建主机Phase 2.5 在 Phase 3 提供创建选项前,对整个租户进行枚举,查找现有的 Custom Host。Phase 3.C-pre / 3.C-pre' 会显示现有主机以供复用。用户必须明确拒绝复用(选择“否”)才能进入创建新主机流程。
本地缓存过期 → 使用已删除的主机Phase 1.0 缓存快速通道会先通过实时 solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$top=1 探测进行验证,然后才复用 — 404/403/超时会回退到完整的 Phase 2
404 含义不明确 → 意外操作Phase 2.2 消歧规则 — 绝不根据单个 404 采取操作;需通过 list-tenant-envs 进行交叉验证
在错误的租户中预配Phase 1.4 回显 tenantId、organizationId 和开发环境 URL;Phase 4.A 在调用前的正文中回显 tenantId
CannotRedirect 被掩盖Phase 2.3 会明确检测此情况并显示特定错误后停止,而不是继续向错误主机写入
JIT 预配未命中 → 静默的 404 链Phase 5 会先针对 instanceApiUrl 调用 WhoAmI,然后才进行任何其他主机操作(检测到现有 PE 时适用)
解决方案过期 → 静默失败Phase 5 读取 PIPELINES_SOLUTION_VERSION,如果低于 MIN_PIPELINES_VERSION 则发出警告
非管理员尝试使用 Custom Host 快速通道Phase 4.A 会预先提示需要管理员角色;遇到 403 时会平稳回退到 Phase 4.B / 4.C
意外创建租户单例 PEPhase 1.4 租户身份验证关卡(回显租户显示名称 + 租户 ID + 开发环境 URL)+ Phase 4.0 调用前确认关卡(在调用前立即再次回显租户显示名称 + 租户 ID)。getOrCreate 端点具有幂等性 — 对已有 PE 的租户调用时,会返回现有 PE(200 + alreadyExisted = true),而不是创建重复项。
遥测数据泄漏所有探测结果都保留在 docs/alm/last-host-check.json 中;关联 ID 是我们生成的标准 x-ms-correlation-id UUID;update-skill-tracking.js 只写入计数器 + 编写工具名称
权限边界所有路径都在用户 OAuth 上下文中运行;403/401 会触发停止,并显示“此操作需要 X 管理员角色”的消息;不会尝试提升权限
速率限制每条路径的总超时时间为 15 分钟;遵循 BAP 返回的 Retry-After;最小轮询间隔为 10 秒
强制链接不可逆不在范围内 — 请参阅此技能不会执行的操作

关键决策点(等待用户确认)

  1. Phase 1.4 — 确认租户身份(只读意图)
  2. Phase 3.A/B/C-pre/C-pre'/C-pre''/C/D/E — 根据 RESOLUTION.status 作出分支决策
  3. Phase 3.C-pre — 是否复用唯一一个现有 Custom Host(是 / 否 / 取消)
  4. Phase 3.C-pre' — 从多个现有 Custom Host 中选择,或创建新的
  5. Phase 3.C-pre'' — 使用现有 PE,或改为创建 Custom Host
  6. Phase 3.C — 选择新建路径(4 个选项:Custom-fast、app-install、PPAC-UI、取消)
  7. Phase 4.A — 自我确认是否具有管理员角色(否 / 不确定 → 回退到 4.B / 4.C)
  8. Phase 4.A — 调用前确认,回显准确的 API 请求正文
  9. Phase 4.B/C — 用户执行 UI 步骤 → 通过“Done — proceed”确认完成
  10. Phase 5 — 如果 Pipelines 解决方案版本低于最低要求,则确认已知悉警告

错误处理

  • verify-alm-prerequisites.js 失败 → 停止并提供修复步骤(az login、pac auth create)
  • BAP 令牌获取失败 → 停止;建议执行 az logout && az login
  • BAP env GET 返回 404 → 执行消歧流程(Phase 2.2 回退方案)
  • BAP env GET 返回 403 → 设置 RESOLUTION.status = "PermissionDenied",显示租户 ID 和环境 ID,然后停止
  • Custom Host 环境创建返回 403 → 无缝回退到 4.B(app install)或 4.C(PPAC UI)
  • Custom Host 环境创建返回其他 4xx → 记录状态和正文,询问用户是重试还是切换路径
  • 生命周期操作轮询超时(默认 15 分钟)→ 询问用户:延长(再等 15 分钟)/ 切换路径 / 退出
  • RetrieveSetting 返回 404 → 视为“没有管理员默认 Custom Host”(当前 discover-pipelines-host.js 的行为)
  • GetOrgDbOrgSetting 返回 404 → 视为“未绑定”(与 UI 行为一致)
  • 对主机执行 WhoAmI,在 JIT 重试后仍返回 403 → 可能是 CannotRedirect 竞争条件或确实存在权限问题;显示两条错误消息并停止
  • 用户声称已安装(4.C)后,verify-host-readiness.js 报告 Pipelines tables not found → 请用户重新检查 PPAC,或延长轮询时间

进度跟踪表

任务主题activeForm描述
检查本地缓存并检测前置条件正在检查缓存并检测前置条件Phase 1.0:读取 docs/alm/last-host-check.json;如果缓存仍有效,则通过 solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$top=1 探测 finalHostEnvUrl——返回 200 时复用并跳转到 Phase 6。否则运行 verify-alm-prerequisites.js + detect-project-context.js;获取 BAP_TOKEN;进入租户身份确认关卡
按解析顺序查找主机正在按解析顺序查找获取 GetOrgDbOrgSetting('ProjectHostEnvironmentId');执行 BAP env GET;如果是 Platform,则检查租户默认 Custom Host;检测 CannotRedirect;如果没有组织绑定,则通过 list-tenant-envs.js 并行列出并探测租户范围内的环境(最多 10 个),查找现有 Custom Host 和 PE;分类为 AvailableUnboundCustomHost / MultipleUnboundCustomHosts / PlatformHostExistsUnbound / NoHost
与用户确认操作正在与用户确认操作按 resolutionStatus 分支;对于 AvailableUnboundCustomHost / MultipleUnboundCustomHosts / PlatformHostExistsUnbound,先显示复用提示;只有用户拒绝复用时,才进入 NoHost 新建流程;收集 Phase 4.A 的明确同意,并在调用前回显请求正文
执行所选路径正在执行所选路径运行路径 A(Custom D365_ProjectHost 环境创建)/B(手动安装应用)/C(PPAC 新建 Custom Host);按 Retry-After 间隔轮询生命周期操作;遵守 15 分钟超时限制
JIT 配置并验证主机正在验证主机对 instanceApiUrl 执行 WhoAmI(仅在检测到现有 PE 时触发 JIT);探测 deploymentpipelines 表;探测 Pipelines 解决方案版本;设置 READY 标志
写入主机检查记录正在写入主机检查记录将完整的 RESOLUTION + actionTaken + correlationId 写入 docs/alm/last-host-check.json;更新技能跟踪记录;显示摘要;建议接下来运行 /power-pages:setup-pipeline

待处理事项(执行阶段解决)

这些事项需要在真实环境中验证:

  1. Pipelines 解决方案的 uniquename。 ✅ 已于 2026-04-28 解决:通过对 SIP 主机(pascalepipelineshost.crm.dynamics.com)进行实时查询,确认值为 msdyn_AppDeploymentAnchor v9.1.2026034.260325188。已存储为 PIPELINES_SOLUTION_UNIQUE_NAME 常量。
  2. MIN_PIPELINES_VERSION。 在测试最低实际使用的解决方案版本中哪些 Pipelines 功能会失效后设置。初步保守估值:"9.0.0.0"。
  3. list-tenant-envs.js 中的 Custom Host 检测标记。 ✅ 已于 2026-04-28 解决:通过 BAP 环境列表实时查询(测试租户中有 1000 个环境)确认:即使使用 $expand=properties.linkedEnvironmentMetadata,也不会返回 linkedEnvironmentMetadata.templates。必须逐个环境进行 Dataverse 探测。探测查询已修正为 solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$select=version&$top=1(一次调用即可检查是否存在并获取版本)。PE 检测仍然很简单,可通过 environmentSku === 'Platform' 判断。
  4. 创建环境时的 BapApiVersion 值。 已为 Pegasus / BAP-RP / Neptune 接入 D365_ProjectHost 模板(根据 eng.ms PR 列表)。PPAC UI 在环境操作中使用 2021-04-01。执行期间通过从 New custom host 捕获新的 HAR 来确认。
  5. 生命周期操作的响应结构。 需要确认 Location URL 返回的是 { properties: { provisioningState } }、{ state },还是两者都有。需通过 HAR 捕获确认。
  6. 租户主区域的发现方式。 4.A 的默认区域。选项包括:BAP_TOKEN 声明(tid、xms_tcdt?)、BAP /tenant?api-version=2021-04-01 端点,或现有环境中的 properties.azureRegionHint。执行期间选择一种。
  7. 冷启动租户测试环境。 需要一个此前未使用过 Pipelines 的租户,以验证端到端流程。个人 MSDN 租户即可。
  8. BAP 环境列表是否支持对 linkedEnvironmentMetadata.templates 进行筛选? 如果支持,我们可以预先筛选出使用 D365_ProjectHost 创建的环境,并在阶段 2.5b 中跳过逐个环境的 Dataverse 探测。待测试。如果不支持,则使用并发数受限的逐个环境探测。
  9. 环境列表响应是否实际包含 linkedEnvironmentMetadata.templates? 如果包含,即使不支持服务器端筛选,我们也可以在客户端轻松筛选。如果不包含,逐个环境的 Dataverse 探测就是唯一的判断依据。通过捕获新的 BAP 环境列表 HAR 来验证,并确保包含 $expand=properties.linkedEnvironmentMetadata 查询参数(resolve-env-by-id.js 已使用该参数)。
  10. 逐个环境探测的速率限制预算。 ✅ 已于 2026-04-28 部分解决:Microsoft 内部测试租户有 1000 个环境(526 个 Production sku,453 个 Sandbox)。即使并发数为 10,逐个探测全部环境也太慢。已采用多层筛选:
    • 预先筛除没有 Dataverse 的环境(在测试租户中约筛除 3 个环境,影响较小)
    • 按 --skus 筛选(默认 Production;始终包含 PE)
    • 按 lastModifiedTime 降序排序
    • 限制为 --maxEnvsToProbe(默认 50;并发数为 10 时,耗时约 5 秒)
    • 达到上限且未找到主机时,显示“已扫描 N 个,共 M 个(筛选条件:...)”警告 待处理:针对典型客户租户(5–50 个环境)验证上限默认值——在这些租户中应该不会产生额外开销。
  11. cacheMaxAgeMs 默认值。 24 小时是初步估值。如果开发租户中的主机频繁变化,可能需要缩短。使其可通过 --cacheMaxAgeHours 配置,并添加文档说明。

脚本

所有脚本均位于 plugins/power-pages/scripts/lib/ 下(或按说明存放)。每个脚本都只负责一项功能,使用 Node,解析 argv,使用 validation-helpers.js 处理 HTTPS,并将 JSON 输出到 stdout。

脚本用途参数输出
check-env-host-binding.js在源环境上执行 POST GetOrgDbOrgSetting('ProjectHostEnvironmentId')--envUrl、--token{ bound, hostEnvId }
resolve-env-by-id.jsBAP 环境 GET,使用 $expand=properties.linkedEnvironmentMetadata,properties.permissions;遇到 401/403 时回退到 PAC shim。--source(auto|bap|pac;默认 auto)、--bapToken(bap 模式必需)、--envId{ found, envId, instanceUrl, instanceApiUrl, displayName, environmentSku, isManaged, permissions, sourceUsed, fallbackReason, ... };遇到 404 时返回 { found: false, reason: "404-ambiguous" };PAC 列表中找不到时返回 { found: false, reason: "not-in-pac-list" }
pac-bap-shim.js将 pac admin list --json 封装成 BAP 格式的环境列表。当 Az→BAP 返回 401 时用作回退方案(某些租户会拒绝 Az CLI 的第一方客户端 ID 访问 BAP)。从 EnvironmentUrl 推导 instanceApiUrl;将 PAC 的 Type 映射到 BAP 的 environmentSku。无法显示 PE(PAC 不会列出 Platform SKU 环境);仅 BAP 提供的字段(tenantId、lastModifiedTime、permissions)返回 null。不适用(CLI 会打印所有环境)BAP 格式的环境数组
list-tenant-envs.js列出环境并逐个探测是否存在 Pipelines(solutions?$filter=uniquename eq 'msdyn_AppDeploymentAnchor'&$select=version&$top=1),最多并行 10 个。预筛选:SKU、是否具有 Dataverse,以及可选的 --includeName。排序:名称提示模式 + 管理员权限 + 最近活动时间。上限:--maxEnvsToProbe(默认 30)。--source(auto|bap|pac;默认 auto)、--bapToken(bap 模式必需)、--skus(默认 Production;始终包含 PE)、--maxEnvsToProbe、--maxConcurrency、--probeTimeoutMs、--includeName、--firstHitWins{ existingCustomHosts[], existingPlatformHost, eligibleForAppInstall[], inaccessibleEnvs[], inaccessibilityBreakdown, totalEnvsInTenant, envsAfterFilter, envsProbed, hitProbeCap, earlyExitOnFirstHit, probeDurationMs, sourceUsed, fallbackReason }
verify-host-readiness.jsWhoAmI(证明身份验证有效,并在 PE 检测路径中触发 JIT)→ 按条件筛选解决方案以查找 msdyn_AppDeploymentAnchor(一次调用即可同时检查是否存在及其版本)--hostEnvUrl、--hostToken、--skipWhoAmI(可选)、--minPipelinesVersion(可选){ ready, pipelinesSolutionVersion, checks: { whoami, solutions }, warnings[] }
provision-custom-host.js使用 D365_ProjectHost 模板、Production SKU 和 CommonDataService databaseType 执行 BAP 环境创建 POST。按 Retry-After 间隔通过 Location 标头轮询生命周期操作。处理 properties.provisioningState / state / status.code 格式。对暂时性 5xx 错误重试。遇到 401/403 时提供明确指引。--bapToken、--displayName、--region、--correlationId(可选)、--timeoutSec(可选,默认 900)、--apiVersion(可选,默认 2021-04-01){ status, envId, instanceUrl, instanceApiUrl, displayName, environmentSku, provisioningState, durationSec, correlationId, pollAttempts, locationHeader }
provision-platform-host.js使用 D365_1stPartyAdminApps 模板和 Platform SKU 执行 BAP getOrCreate。在 200 幂等路径(返回现有 PE)时返回 alreadyExisted: true,在 202 + Location 轮询路径(新建环境)时返回 alreadyExisted: false。供 Phase 4.0 使用。--bapToken、--correlationId(可选)、--timeoutSec(可选,默认 600)、--apiVersion(可选,默认 2021-04-01)、--bapBase(可选){ status, alreadyExisted, envId, instanceUrl, instanceApiUrl, displayName, environmentSku, provisioningState, durationSec, correlationId, pollAttempts, locationHeader }
install-pipelines-app.js在现有环境中发现并安装 Power Platform Pipelines 应用程序包。解析方式:BAP applicationPackages LIST + /install POST → 200 同步完成 / 202 + Location 轮询;遇到 401/403/5xx 时回退到 PAC CLI(pac application install --environment-id ... --application-list msdyn_AppDeploymentAnchor)。将安装 POST 返回的 409 视为幂等(已安装)。可选的安装后 Dataverse 验证探测。供 Phase 4.B 使用(于 2026-05-08 替代手动点击 PPAC 完成安装)。--bapToken、--envId、--instanceApiUrl(可选,用于验证)、--hostToken(可选,用于验证)、--no-pac-fallback(可选;默认启用 PAC 回退)、--correlationId(可选)、--timeoutSec(可选,默认 600)、--apiVersion(可选,默认 2022-03-01-preview)、--bapBase(可选){ status, alreadyInstalled, installPath: 'bap'|'pac'|'cached', packageUniqueName, pipelinesSolutionVersion, durationSec, correlationId, pollAttempts, locationHeader, pacFallbackReason }
ensure-pipelines-host-detect.js仅用于检测的编排包装器。运行 Phase 1.0(缓存快速路径)+ Phase 2(包含租户范围枚举的解析顺序)+ Phase 5(若找到主机则验证)。始终输出 actionTaken: "none"。供 plan-alm Phase 1 和 setup-pipeline Phase 1 使用。--source auto(默认)会先尝试 BAP;遇到 401/403 时回退到 PAC CLI shim——可用于 Az CLI 令牌被 BAP 拒绝的租户。--envUrl、--token、--userId、--bapToken(与 --source auto 或 pac 搭配时可选)、--source(auto|bap|pac)、--projectRoot、--cacheMaxAgeHours(可选)、--no-cache、--includeName、--maxEnvsToProbe、--skus、--minPipelinesVersiondocs/alm/last-host-check.json 架构(包含 sourceUsed、fallbackReason)
validate-ensure-host.js(技能 scripts/)PostToolUse Stop 钩子验证器。兼容架构 v1+v2。将 CannotRedirect / OrgSettingStale / PermissionDenied 视为已记录的终止错误状态(即使主机不可用,技能仍视为运行成功)。不适用(从 stdin JSON {cwd} 读取)退出码 0(批准)或 退出码 2(阻止)
已复用的现有辅助脚本(未作更改):
  • verify-alm-prerequisites.js
  • detect-project-context.js
  • discover-pipelines-host.js(租户默认自定义主机探测;由封装脚本在阶段 2 的步骤 3 中调用)
  • update-skill-tracking.js

验证脚本

skills/ensure-pipelines-host/scripts/validate-ensure-host.js(由集中式 PostToolUse hook 在 Skill 工具上运行;由于 skills/ensure-pipelines-host/SKILL.md 存在,该技能会自动被跟踪——请参阅 scripts/lib/powerpages-hook-utils.js):

  • 如果当前工作目录中没有 docs/alm/last-host-check.json → 退出码为 0(不是 ensure-host 会话)。
  • 如果存在:验证 schemaVersion === 1 或 2(向前兼容);必填字段均已填充(tenantId、sourceEnvUrl、resolutionStatus);对于非终端错误状态,ready === true;当 ready === true 时,finalHostEnvUrl 已填充。
  • 终端错误状态(CannotRedirect / OrgSettingStale / PermissionDenied)在 ready: false 时可接受——技能已成功运行,并指出了需要手动处理或由管理员解决的状态。
  • candidates 区块(v2)为可选项——缺少该区块不会导致验证失败。

配套的提示词 hook 会检查:

  1. 是否满足以下任一条件:(a) 阶段 1.0 的缓存快速路径成功,并复用了缓存的主机;或 (b) 完整流程已运行:
    • 已确认租户身份门控。
    • 已通过完整解析顺序确定 RESOLUTION.status,其中包括在源环境未绑定时枚举整个租户。
    • 如果状态为 AvailableUnbound*、MultipleUnboundCustomHosts 或 PlatformHostExistsUnbound,用户已明确选择复用或新建。
    • 如果状态表明需要预配(NoHost),则已完成用户明确选择的路径(actionTaken 为 fast-path-*、user-installed-* 或 user-created-* 之一——即不是 "none",也不是 "reuse-existing-*")。
  2. verify-host-readiness.js 报告 ready: true(或已达到有记录的终端错误状态)。
  3. 已写入 docs/alm/last-host-check.json,且其 schemaVersion 为 2;如果运行了整个租户枚举,则已填充 candidates 区块。
  4. 已呈现摘要。

评论

支持 Markdown