mcpField
U
upgrade-mule-app

upgrade-mule-app

github.com/mulesoft开发14152 次安装今天更新
代码仓库

关于 upgrade-mule-app

upgrade-mule-app 是用于现代化 Mule 应用的技能,可更新 Java、Mule Runtime 和连接器依赖并处理兼容性,适合具备所需项目文件及 Anypoint CLI 环境的开发者。

主要功能

  1. 01检测当前 Mule 和 Java 版本
  2. 02推荐兼容的升级目标
  3. 03检查连接器的 Java 兼容性
  4. 04查找适配目标运行时的连接器版本
  5. 05更新项目运行时和连接器配置

适用场景

  1. 01将 Mule 应用升级到较新 Java 版本
  2. 02将应用迁移到较新 Mule Runtime
  3. 03现代化过程中解决连接器兼容性问题

安装

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

$npx skills add https://github.com/mulesoft/mulesoft-dx --skill upgrade-mule-app

安装后在 Claude Code 中输入/upgrade-mule-app确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

lib/
anypoint.mjs
fsx.mjs
platform.mjs
pom-edit.mjs
xml-flow-dom.mjs
xml-flow.mjs
references/
llm-prompts.md
scripts/
_pom_utils.mjs
apply_connector_pin.mjs
apply_parent_pom_fork.mjs
apply_runtime_bump.mjs
check_connector_java_compat.mjs
describe_connector.mjs
detect_current_java_version.mjs
detect_current_mmp_version.mjs
detect_current_mule_version.mjs
enumerate_usage_xml.mjs
enumerate_usage.mjs
extract_connectors.mjs
promote_new_connector_pins.mjs
resolve_jdk.mjs
resolve_runtime.mjs
resolve_target_connectors.mjs
resolve_target_versions.mjs
set_plugin_version.mjs
validate_prerequisites.mjs
verify_dependency_tree.mjs
verify_metadata_coverage.mjs

详细介绍

Mule 应用升级器

通过自动更新版本并端到端解决兼容性问题来升级 Mule 应用。

何时使用此技能

用户提出以下请求时使用此技能:

  • “升级我的 Mule 应用 Java 版本”
  • “升级 Mule 运行时”
  • “升级 Java 和 Mule 运行时”
  • “现代化 Mule 应用”
  • “为新 Java/运行时更新依赖项”

**触发关键词:**升级、迁移、更新、现代化 · Java、Java 版本 · Mule 运行时、运行时版本 · 依赖项、兼容性。


前提条件

anypoint-cli-v4 --version
anypoint-cli-v4 dx --help
anypoint-cli-v4 plugins        # shows the installed DX plugin version
anypoint-cli-v4 conf

如果缺少工具:

npm install -g @mulesoft/anypoint-cli-v4
npm install -g @salesforce/anypoint-cli-dx-mule-plugin@latest
anypoint-cli-v4 conf username <username>
anypoint-cli-v4 conf password <password>

需要 DX Mule 插件 @salesforce/anypoint-cli-dx-mule-plugin ≥ 1.3.0。 此技能会在升级过程中通过 dx mule jdk download(步骤 3b 和 13)自动配置源 JDK 和目标 JDK;此子命令仅存在于 1.3.0+ 版本中。较旧的插件虽然能通过 dx --help 存在性检查,但会在下载 JDK 时失败。anypoint-cli-v4 plugins 会显示已安装版本(例如 @salesforce/anypoint-cli-dx-mule-plugin 1.3.1);如果版本低于 1.3.0,请升级到最新版本:

npm install -g @salesforce/anypoint-cli-dx-mule-plugin@latest

步骤 1 中的 validate_prerequisites.mjs 会自动强制执行此最低版本要求,并在不符合要求时提示上述升级命令。

**要求:**Mule Runtime 4.3+ 和 Java 8+。不支持低于任一版本的应用,请先升级到基准版本。

项目中需要的文件:

  • mule-artifact.json - Mule 应用元数据
  • pom.xml - Maven 配置
  • 父 POM(如果在 pom.xml 中引用)

随附脚本

此技能附带 scripts/ 目录下的小型 Node.js(ESM、零依赖)脚本。请通过 Bash 工具调用这些脚本,不要将其内容直接写入回复。脚本会将输出保存到磁盘,供后续步骤以程序化方式读取,避免依赖 Bash 工具调用返回后便会消失的 shell 变量:

脚本用途输出位置
scripts/validate_prerequisites.mjs步骤 1 — 验证应用目录(pom.xml + mule-artifact.json)、父 POM 是否可用(如果有引用)、Anypoint CLI v4、DX 插件 ≥ 1.3.0(从 anypoint-cli-v4 plugins 读取版本;dx mule jdk download 需要此插件),以及本地 Maven 是否为 3.9.x 系列(MMP 4.x + MUnit 需要;只检测并提示,不安装)。仅验证;当 errors[] 非空时以非零状态退出tmp/upgrade-prereqs.json(包含 inAppDir、parentDeclared、parentFound、cliPresent、dxPluginPresent、dxPluginVersion、dxPluginInRange、mavenVersion、mavenInRange、errors[] 等)
scripts/detect_current_mule_version.mjs步骤 2a — 从 app.runtime 属性确定当前 Mule Runtime 版本,先搜索子级 pom.xml,再搜索其完整本地父级链(父级、祖父级等),并根据合并后的链解析 ${...};标记低于支持下限(4.3)的版本。--user-version <v> 可保存用户提供或修正的值(也会通过 belowFloor 标记)tmp/current-mule-version.json(包含 version、source、resolvedFrom("child" | "parent" | "ancestor")、needsUserPrompt、belowFloor、minSupportedVersion、warnings[] 等)
scripts/detect_current_java_version.mjs步骤 2b — 从 mule-artifact.json 的 javaSpecificationVersions 确定当前 Java 版本,并标记低于支持下限(8)的版本。--user-version <n> 可保存用户提供或修正的值(也会通过 belowFloor 标记)tmp/current-java-version.json(包含 version、source、supportedVersions、needsUserPrompt、belowFloor、minSupportedVersion、warnings[] 等)
scripts/resolve_jdk.mjs步骤 3 和阶段 2 — 确保指定 Java 主版本对应的 JDK 可用,并报告可用的 JAVA_HOME。通过 dx mule runtime list(回退至矩阵文件)将主版本解析为完整构建字符串(例如,8 → 8.0.472_8),复用 Anypoint Code Builder java 目录下已安装的 JDK,仅在没有可用版本时下载。除非使用 --no-download,否则可能下载(需要网络)tmp/resolve-jdk-<major>.json(包含 major、requestedBuild、javaHome、javaBin、source、downloaded、available、errors[] 等)
scripts/detect_current_mmp_version.mjs步骤 3c(构建前)— 检测应用的当前 mule-maven-plugin 版本(先从子级 pom.xml 读取,再沿本地父级链读取,并解析 ${...}),以便步骤 3c 判断是否需要升级基线构建的 MMP。当前 MMP 低于 4.x 时设置 needsPluginBump: true(3.x 无法在所需的 Maven 3.9.x 上运行 → 仅为基线构建升级至最新 4.x)。只检测 MMP 版本;Maven 兼容性由步骤 1 的 Maven 3.9.x 前置检查负责。仅验证;不下载、不修改tmp/current-mmp.json(包含 pluginVersion、pluginMajor、pluginDefinedIn、needsPluginBump、errors[]、warnings[] 等)
scripts/set_plugin_version.mjs步骤 3c 情形 B(MMP 为字面值)— 当 -D 无法覆盖时,在 pom.xml 中确定性地设置插件的硬编码 <version>。按 artifactId(以及声明时的 groupId)匹配插件块,只重写该块的 <version>,不受空白字符或制表符影响;保留 ${property} 版本不变(这是 -D 的情形 A 路径),并安全处理包含多个插件块的 POM(build/plugins + pluginManagement)。调用方会在临时基线构建后还原 POM。编辑成功或无需更改时退出码为 0;未找到或出错时退出码为 1stdout JSON {artifactId, version, groupId, edits[]}(会修改 pom.xml)
scripts/resolve_runtime.mjs步骤 13 — 确保目标 Mule Runtime 分发包已就绪,并报告其 runtimePath。复用 Anypoint Code Builder runtime 目录下已安装的 mule-enterprise-standalone-<version>,否则通过 dx mule runtime download 下载——仅当 dx mule runtime list 提供该版本时才会下载。除非使用 --no-download,否则可能下载(需要网络)。绝不设置运行时路径tmp/resolve-runtime-<version>.json(包含 version、resolvedVersion、runtimePath、source、downloaded、available、errors[] 等)
scripts/resolve_target_versions.mjs步骤 4 — 根据当前版本和实时 dx mule runtime list 确定建议的升级目标(同渠道内:最高次要版本、最新补丁版本、最新的非 EOL Java),并根据锁定策略验证用户请求的目标(TARGET_MULE/TARGET_JAVA)。仅提供建议——始终以 0 退出;由调用方根据字段分支处理tmp/target-versions.json(包含 currentMule、currentJava、channel、options[]、requestedTarget {accepted、mule、java、reasonCode、reason、crossChannel、warning、belowRecommended、note}、requestedJavaOnly {java、supported、supportedJavas[]、recommendedMule、recommendedJava、note}、nothingToUpgrade、needsUserPrompt、warnings[] 等)
scripts/extract_connectors.mjs步骤 5a — 从应用的 pom.xml 及完整本地祖先链(父级、祖父级等)中提取连接器依赖(<classifier>mule-plugin</classifier>,非 test 作用域),并从本地 POM 解析各版本:内联值、${...}(单层、嵌套或组合形式,如 ${major}.${minor})、继承的 <dependencies>,以及由任一本地 <dependencyManagement>(当前 POM 自身或祖先 POM)管理的无版本依赖。确定性静态解析;不调用 CLI、不访问网络。仅提供建议——始终以 0 退出tmp/connectors.json(包含 connectors[] {nick、groupId、artifactId、version、versionResolved、resolvedFrom、versionManagedIn?}、excluded[](test 作用域)、customLibraries[](非连接器应用 JAR——在步骤 12 提示操作人员,绝不自动升级)、needsUserPrompt、warnings[] 等)
scripts/check_connector_java_compat.mjs步骤 5b — 对步骤 5a 中的每个连接器执行 exchange asset describe <groupId>/<assetId>/<version>(精确查询,带重试),并读取其 is-java-*-supported 标签,报告当前使用版本支持的 Java 版本。当连接器无法验证时会硬性停止(退出码 1,stop: true):describe 失败且精确 GA 的 asset list 探测未找到匹配资源(确实不在 Exchange 上),或不存在 is-java-* 标签。当 describe 失败但资源存在时(仅固定的版本已从列表移除),会设置 currentVersionDelisted: true,且不会阻止继续tmp/connector-java-compat.json(包含 connectors[] {nick、groupId、artifactId、version、supportedJava[]、blocked、blockReason}、blocked[]、stop、warnings[] 等)
scripts/resolve_target_connectors.mjs步骤 6 — 为步骤 5 中的每个连接器查找同时支持目标 Mule 和目标 Java(来自步骤 4)的最新已发布版本。每个连接器调用一次 exchange asset describe,即可获取所有同级版本及各自的 min-mule-version / is-java-*-supported 标签;在本地筛选 is-java-<targetJava>-supported == true 且 min-mule-version <= targetMule 的版本,然后按 semver 选出最高版本。如果 describe 当前版本失败(已从列表移除),则改用 asset list 中最新的 GA 同级版本执行 describe,以便仍能读取 .versions[]。当连接器没有兼容目标的版本,或确实不在 Exchange 上时,会硬性停止(退出码 1,stop: true)。目标来自 TARGET_MULE/TARGET_JAVA 环境变量或 tmp/target-versions.json 的 options[0]tmp/target-connectors.json(包含 targetMule、targetJava、connectors[] {nick、groupId、artifactId、currentVersion、targetVersion、changed、candidateCount、minMuleVersion、supportedJava[]、blocked、blockReason}、blocked[]、stop、warnings[] 等)
scripts/_pom_utils.mjs共享库(步骤 1–6)— 容错 XML 解析器、${...} 属性解析(单层/嵌套/组合形式,带循环保护)、本地父 POM 定位(并验证父级身份),以及跨本地祖先链的托管版本查询。供以上检测/验证/提取脚本使用。不直接调用(已导入)
describe_connector.mjs对新连接器版本执行模式 A/B/C describe(摘要、按操作、按配置提供程序)。调用方式(使用标志,不使用位置参数):模式 A <nick>-new;模式 B <nick>-new --type operation --name <op>(或 --type source --name <src>);模式 C <nick>-new --type connection-provider --name <provider> --config-name <config>。参见步骤 7(7a 模式 A、7b 模式 B/C)。tmp/connector-metadata/<nick>-new.json、<nick>-new-<op>.json、<nick>-new-<config>-<provider>.json
enumerate_usage_xml.mjs首选使用情况枚举器——使用 fast-xml-parser 解析 src/main/mule/**/*.xml。输出与 enumerate_usage.mjs 相同,但能正确处理混乱输入(忽略注释掉的元素;将 config-ref 绑定到其所属元素)。如果无法导入 fast-xml-parser,则以 rc=3 退出 → 调用方回退到 grep 脚本。参见步骤 7“使用情况枚举”。tmp/connector-usage/<nick>.json
enumerate_usage.mjs零依赖(regex/grep)使用情况枚举器——作为 enumerate_usage_xml.mjs 的回退方案。扫描 src/main/mule/**/*.xml,查找应用使用的连接器操作、配置、错误类型和命名空间前缀。旧版本一侧的事实来源——取代重新描述旧连接器版本。参见步骤 7b(使用情况枚举 + 输出结构)。tmp/connector-usage/<nick>.json
apply_connector_pin.mjs在 pom.xml 中升级一个连接器的版本,并重写每个流程 XML 中的 xsi:schemaLocation。读取 tmp/connector-choices/<nick>-new.json(必需的 GAV)和 tmp/connector-metadata/<nick>-new.json(命名空间元数据,可选——纯 POM 连接器不会有此文件,这种情况下 XSD 重写不执行)。操作具有确定性——绝不手动编辑 xsi:schemaLocation。修改 pom.xml + src/main/mule/**/*.xml
apply_runtime_bump.mjs确保存在 <app.runtime>(升级已有值,或在缺失时插入);仅在 <javaVersion>、java.version、jdk.version 已存在时升级(绝不插入);将 maven-compiler-plugin 级别原地规范化为目标 Java——升级其所在位置的 maven.compiler.{source,target,release} 属性或内联 <source>/<target>/<release>(仅有内联 <target>/<source> 时,将其转换为 <release> 来补全;release 绝不与 source/target 共存);仅当应用包含自定义 Java(src/main/java/src/test/java),且任何位置均未声明级别时,才插入 maven.compiler.release;升级/插入 <mule.maven.plugin.version>;升级 pom.xml 中的每个 MUnit 版本位置(<munit.version> 属性、munit-maven-plugin 插件块,以及 munit-runner/munit-tools 依赖);在 munit-maven-plugin 配置中插入 <runtimeVersion>${app.runtime}</runtimeVersion>(仅在缺少时插入,绝不覆盖现有固定值);将 minMuleVersion 设为**x.y.0 功能线**(符合平台规范——与 ACB/Studio 一致,Introspection Service 需要次要版本线格式),并确保 mule-artifact.json 的 javaSpecificationVersions 包含目标 Java。runtimeVersion 固定值可在不导致测试退化的前提下使用正确的功能线下限:否则 MUnit 的内嵌运行时默认为 minMuleVersion,而 x.y.0 下限会启动较旧的运行时,导致带有 JAVA_25 注解的连接器因 EnumConstantNotPresentException 失败。所有值均从 tmp/upgrade-targets.json(.mule.to / .java.to / .muleMavenPlugin.to / .munit.to)读取——不硬编码。.muleMavenPlugin.to 和 .munit.to 在步骤 11a 中实时解析;如果任一值缺失,相应版本保持不变。仅重写版本——不运行 java(构建通过步骤 16 中内联的 JAVA_HOME=... mvn 前缀固定到已解析的目标 JDK)。修改 pom.xml + mule-artifact.json
promote_new_connector_pins.mjs将每个 tmp/connector-choices/<nick>-new.json 复制到 tmp/connector-versions/<nick>.json,以供阶段 2 的固定脚本使用。在运行 apply_connector_pin.mjs 之前执行一次。tmp/connector-versions/<nick>.json
apply_parent_pom_fork.mjs父 POM 所属的连接器版本——与读侧链遍历相对应的写侧操作,分为两个阶段。范围仅限连接器:app.runtime / mule.maven.plugin.version / Java 属性属于应用作用域,始终由 apply_runtime_bump.mjs 写入子级(绝不分叉至共享祖先)。--phase=edit(步骤 14): 对拥有连接器(位于其 <dependencies>/<dependencyManagement> 中)的每个本地祖先,将该祖先拥有的所有连接器版本升级至已解析的目标——沿 ${property} 引用追踪;处理该祖先中声明的所有连接器(分叉范围内的全部连接器——应用未使用且没有已解析目标的连接器只发出警告,绝不硬性停止)。保持祖先自身的 <version> 和子级的 <parent> 引用不变,以便本地构建通过 <relativePath> 解析新版本。--phase=fork(步骤 18,构建成功后): 升级每个拥有连接器的祖先自身的 <version>(根据原始的 tmp/pom-backups/ 快照计算,因此可幂等执行),并更新下游的 <parent><version>。两个阶段都按从最深层向上的顺序处理并验证——从子级视角重新解析每个连接器(使用与提取器相同的遍历方式),并在分叉阶段确认每个 <parent> 链接都指向分叉版本。读取 tmp/connectors.json(来源信息)和 tmp/target-connectors.json(目标版本)。--phase=edit|fork(默认 edit);--fork-bump=major|minor|patch(默认 minor,仅分叉阶段);--dry-run 仅打印计划,不写入。验证失败或输入缺失/错误时退出码为 1;未修改任何拥有连接器的祖先时作为空操作以 0 退出。不发布至 Exchange——仅进行本地编辑。修改祖先的 pom.xml(edit 阶段:托管的连接器版本;fork 阶段:自身的 <version> + 下游的 <parent> 引用);完整结果 JSON {phase, ancestorsForked[], edits[], backedUp[], verify{}, warnings[]} 写入 tmp/parent-pom-<phase>[-dryrun].json(edit 阶段会在首次写入前,将每个要编辑的祖先快照至 tmp/pom-backups/,仅在文件不存在时创建);stdout 仅输出简短摘要(避免较长链条导致内容截断)
verify_metadata_coverage.mjs步骤 11.5 门禁——针对 tmp/connector-usage/*.json 中的每个操作/源/提供程序,验证 tmp/connector-metadata/ 中是否存在对应的模式 B/模式 C JSON。缺少任何必需的按操作/按提供程序 describe 时,以 FAIL 行和退出码 1 退出。模式 A 的 .connectionProviders[] 为空的配置(D7 回退方案——某些数据库配置)会输出 INFO,不会导致失败;阶段 C 会直接读取这些配置的模式 A .configs[]。可选的 --strict 也会在出现 WARN 行时失败(已重命名/移除的操作缺少 <nick>-op-renames.json 条目)。stdout FAIL/WARN/INFO 行
verify_dependency_tree.mjs将静态 POM 遍历发现的内容与 Maven 实际解析的内容交叉核对(mvn dependency:tree,限定为连接器 GAV)——它是已解析值的权威来源,可捕获本地遍历无法发现的导入 BOM/传递依赖/过时的 ~/.m2 来源。两道门禁,共用一个引擎:步骤 12.0 --against=existing 比较当前版本与 tmp/connectors.json(软性——在计划中标注,并在审批时提示);步骤 14.5 --against=target 比较刚写入的固定版本与 tmp/target-connectors.json(硬性——构建前停止)。--expected=<path> 可覆盖文件路径。由调用方决定执行时机:existing 在步骤 14 写入之前,target 在写入之后。全部匹配时退出码为 0;操作错误时退出码为 1(Maven 缺失/依赖树失败/expected 文件无法读取——始终停止);不匹配时退出码为 2 — 由调用方决定软性或硬性处理。tmp/dep-tree-verify-<mode>.json(matched[]、mismatches[]、missingFromTree[]、resolvedOnlyByTree[])
共享辅助工具位于 scripts/ 旁的 lib/*.mjs 中:anypoint.mjs(清理 CLI 环境变量)、fsx.mjs(I/O)、platform.mjs(解析 Java 版本)、pom-edit.mjs(重写 pom.xml + mule-artifact.json + XSD)、xml-flow.mjs(flow XML grep 基础工具)。步骤 1–6 的检测、验证和提取脚本共用 scripts/_pom_utils.mjs(容错 XML + ${...} + 父级 POM 定位 + 托管版本查找);步骤 7–20 的编辑脚本使用 lib/pom-edit.mjs——这两个 POM 辅助工具彼此独立。

调用脚本时,请使用“skill is now active”消息中提供的绝对路径(该路径是包含此 SKILL.md 的目录)。不要构造 ../scripts/... 之类的相对路径——Cline 的工作目录会在不同轮次间变化,实际运行中相对路径曾导致“No such file or directory”错误。下方内联步骤示例将 scripts/... 写作简写;实际执行时,请替换为 <skill-dir>/scripts/...。

**为何使用脚本而非内联 bash:**将数据写入磁盘文件,可以让数据在不同响应间保留。Bash 工具调用返回后,Shell 变量会消失,但文件会保留,后续步骤可以读取。


工作流结构(两个阶段)

此工作流分为两个阶段,中间设有必须经过的用户审批关卡。

  • 阶段 1:计划(步骤 1–12)。 验证前置条件、获取当前版本、构建基线、确定目标版本、提取连接器并检查其当前 Java 兼容性、解析与目标兼容的连接器版本、分析插件/DataWeave/MUnit 兼容性、展示升级计划、等待用户批准。阶段 1 不会写入项目文件——所有产物都存放在相对于工作区的 tmp/ 目录下。在获得批准之前,不会修改 mule-artifact.json、pom.xml 或流程。

  • 阶段 2:执行(步骤 13–20)。 下载运行时/Java、更新版本、更新应用代码(flows/配置/DW/自定义 Java)、运行构建循环、运行 MUnit 循环、清理工作区、声明完成。只有阶段 2 会修改项目文件。

第 12 步的审批关卡必须明确通过后,才能开始阶段 2。跳过计划或在获得批准前修改文件,都会违背两阶段结构的目的。


全工作流规范(请在第 1 阶段前阅读)

  • 构建 → 清理 → 完成信号分离。 按顺序分别回复,每次只调用一个工具:先运行 mvn clean package,然后运行 rm -r tmp/,接着运行 rm -rf 删除临时的 node_modules(第 19 步),最后发送完成信号。不要将它们合并——绝不要把两个 rm 命令串在一行中。每次都要等待结果返回后再继续。
  • 每条回复只运行一次 mvn。 修复后重新运行构建时,该条回复中只能输出 mvn 命令。不要将其与后续编辑、后续 Shell 命令或完成信号合并。
  • “完成”表示构建已通过。 只有在运行了 mvn clean package 的回复返回 BUILD SUCCESS,且 mvn test 返回所有测试均通过后,才能宣布完成。
  • 使用随附脚本——不要重新实现它们。 如果某个步骤附带脚本(见“随附脚本”),请运行该脚本并读取其 JSON 输出。不要用原始的 anypoint-cli-v4 exchange asset list/describe + jq 手动实现其逻辑,也不要自行查询 Exchange 来“验证”或“二次确认”脚本的结果。脚本是事实依据;它们使用精确的 asset describe 查询(当某个版本已下架时,仅在内部使用 asset list 精确探测 GA 是否存在),而临时使用 asset list 选版本会遇到模糊匹配和分页问题(它会静默漏掉版本并返回同级资产),从而导致错误结果。如果脚本似乎有误,请说明情况并停止——不要绕开它。尤其是,第 6 步连接器的目标版本只能来自 resolve_target_connectors.mjs。
  • 只能通过脚本/CLI 解析版本。 所有版本都必须来自随附脚本(若某步骤没有脚本,则来自该步骤指定的 CLI 命令),绝不能硬编码。切勿凭记忆或文档填写版本。
  • 一次只做一个步骤。 只完成当前步骤的工作,然后停止。不要提前收集后续步骤所需的数据(例如,在第 6 步期间查看插件版本或审查 flow/DataWeave)——每个步骤都有自己的脚本和说明。
  • 每次调用 describe_connector.mjs 都必须使用 Java 17+。 在 Java 8 或 11 下,Anypoint CLI 的 dx mule describe-connector 仍会以 0 退出,但返回 DEGRADED 响应——configs[] 会缩减为 {name, connectionProviders: []},不包含 parameters / attributes,从而悄无声息地隐藏必需属性方面的破坏性变更。技能的 Phase-C diff 随后会对实际已损坏的配置签字放行,而 mvn 会在 process-classes 阶段因 XSD 验证错误而失败(cvc-complex-type.4: Attribute 'X' must appear on element '<prefix>:<config>')。在第 7 步调用 describe_connector.mjs(Mode-A/B/C)之前,或在第 16 步的第 6 类中延迟重新描述单个连接器之前,请导出 Java 17+ 的 JAVA_HOME(见第 13 步)。脚本自身会拒绝在低于 Java 17 的环境中运行并给出修复提示,因此过时的 JAVA_HOME 会立即被发现,而不是拖到七步后的打包阶段。
  • 跳过 not_in_use——这是 Mode-B/C 之前唯一允许的跳过情况。 如果第 7 步的 enumerate_usage.mjs 在 stdout 上输出 not_in_use JSON,说明该连接器已在 pom.xml 中声明,但没有任何 flow 使用。将该连接器的计划缩减为“仅升级 pom 版本——不编辑 flow,也不按操作逐项 describe”。跳过 Mode-B 和 Mode-C,但仍须将该连接器保留在计划的 pom-only 部分,以便第 2 阶段仍运行 apply_connector_pin.mjs。不要擅自增加其他“稳定连接器”的短路规则——对于每个实际在用的连接器,都必须无条件运行 Mode-B / Mode-C,并让第 12 步的计划综合通过在 Mode-B / Mode-C JSON 中找不到任何逐符号差异,自然得出“无需重写”。

第 1 阶段:计划

第 1 步:验证前置条件

运行前置条件验证脚本。它只进行验证,不会写入项目,也不会提示输入:

node scripts/validate_prerequisites.mjs .

验证结果会写入 tmp/upgrade-prereqs.json(使用 jq 读取字段,例如 jq -r '.parentFound' tmp/upgrade-prereqs.json)。**如果脚本以非零状态退出(即 errors[] 不为空),请停止,并先处理错误再继续。**最常见的错误包括:

  • 不在应用目录中(缺少 pom.xml / mule-artifact.json)→ 告知用户从 Mule 应用根目录运行。
  • 声明了祖先 POM,但本地未找到 → 检查会遍历完整的 <relativePath> 链(父级 → 祖父级 → …),并在 ancestorChain[] 中列出找到的每个 POM,因为版本检测(第 2/5 步)和第 2 阶段编辑都需要完整链路(连接器版本可能位于祖父级;第 14/18 步会编辑并分叉所有拥有相关配置的祖先 POM)。此处任何深度缺少一环都是严重错误(立即父级缺失时 parentFound: false;更深层级缺失仍会填入 errors[])——不要让问题拖到第 5 步。请用户将缺失的 POM 放到本地相对路径下(可通过声明该 POM 的 <parent><relativePath> 解析,或使用默认路径 ../pom.xml),然后重新运行。不要尝试下载它。
  • 缺少工具链(cliPresent / dxPluginPresent 为 false)→ 指引用户查看前置条件中的安装命令。
  • DX 插件版本过旧(dxPluginPresent: true,但 dxPluginInRange: false)→ 已安装的 @salesforce/anypoint-cli-dx-mule-plugin(dxPluginVersion)版本低于 1.3.0,因此无法使用 dx mule jdk download(第 3b/13 步)。错误信息会给出修复方法:npm install -g @salesforce/anypoint-cli-dx-mule-plugin@latest。升级之前不要继续。
  • Maven 不属于 3.9.x 系列(mavenInRange: false)→ MMP 4.x 和 MUnit 要求 Maven 3.9.x,基线构建(第 3c 步)会在 MMP 4.x 上运行。告知用户切换到 Apache Maven 3.9.x 发行版(在本次会话中将其 bin/ 放到 PATH 最前面),然后重新运行。不要下载或自动安装 Maven,也不要建议仅用包管理器执行 install maven——这会安装最新版(通常是 4.x 或旧版 3.6.x),导致此检查失败。Maven 是标准开发工具链,与 CLI/DX 插件一样视为前置条件。

只有脚本以零状态退出后,才能继续第 2 步。

此处不要要求 JAVA_HOME 指向 Java 17。第 3 步会使用应用当前的 Java 版本构建(通常是 8 或 11);第 13 步才会检查 Java 17。


第 2 步:获取当前版本

2a. 当前 Mule Runtime 版本

运行检测脚本(不要直接解析 POM——脚本会先读取子项目中的 app.runtime 属性,再读取父级 pom.xml,并解析 ${...}):

node scripts/detect_current_mule_version.mjs .

它会将结果写入 tmp/current-mule-version.json。使用 jq 从文件中读取字段(例如 jq -r '.version' tmp/current-mule-version.json),并根据结果执行相应操作:

  • belowFloor: true → 检测到的 version 低于 Mule Runtime 的最低支持版本(minSupportedVersion,当前为 4.3)。此应用不在适用范围内——没有可用于提示的有效版本。停止,并告知用户先将应用升级到至少该版本,再运行此技能(参见 warnings)。

  • 已设置 version,且 needsUserPrompt: false → 将 version 作为当前 Mule Runtime 版本。继续。

  • needsUserPrompt: true → 脚本无法确定可信的版本。检查 warnings:

    • 已声明父级项目,但本地未找到 → 请用户在本地提供父级 POM,然后重新运行此步骤。不要尝试下载。
    • 否则(无法解析任何版本)→ 通过 AskUserQuestion 向用户询问当前 Mule Runtime 版本(标题 Current Mule version,问题 “I couldn't detect your app's current Mule Runtime version. Which version does it run on today?”)。提供当前常见运行时版本系列作为选项(例如 4.3.0、4.4.0、4.6.x),让用户选择,而不是手动输入;用户也可以通过“Other”选项提供其他版本。如果用户无法提供,停止。

    然后通过使用 --user-version 重新运行脚本,持久化用户的回答,以便后续步骤从文件中读取真实值(而非 null)——在将答案写回之前,它只存在于本次对话中:

    node scripts/detect_current_mule_version.mjs . --user-version <v>
    

这会用 version: <v>、source: "user-supplied" 重写 tmp/current-mule-version.json;如果 <v> 低于最低版本(4.3),还会设置 belowFloor: true——若出现这种情况,请按上述停止。

最低版本限制同样适用于用户提供的版本。 使用 --user-version 重新运行脚本时,会自动应用最低版本限制(belowFloor: true)——如果标记为低于最低版本,则此应用不在适用范围内:停止,并告知用户先将应用升级到至少 Mule 4.3。

检测来源(由脚本实现):app.runtime 属性——先检查子项目的 pom.xml,再检查父级 pom.xml,并根据合并后的子级与父级属性解析 ${...}。这是 MRT 版本的唯一来源。无法解析的引用会转而提示用户,而不会被直接接受为字面值。

2b. 当前 Java 版本

运行检测脚本(不要内联读取 mule-artifact.json——脚本会读取 javaSpecificationVersions):

node scripts/detect_current_java_version.mjs .

脚本会将结果写入 tmp/current-java-version.json。使用 jq 读取文件中的字段(例如 jq -r '.version' tmp/current-java-version.json),并据此分支处理:

  • belowFloor: true → 检测到的 version 低于最低支持的 Java 版本(minSupportedVersion,当前为 8)。此应用不在适用范围内——停止,并告知用户先将应用升级到至少 Java 8,再运行此技能(参见 warnings)。

  • 已设置 version,且 needsUserPrompt: false → 将 version 用作当前 Java 版本。继续。

  • needsUserPrompt: true → 检查 warnings / supportedVersions,并通过 AskUserQuestion 提问(标题为 Current Java version)——绝不能使用自由文本:

    • supportedVersions 中有多个条目 → mule-artifact.json 声明支持多个 Java 版本;提问 "Your app declares support for multiple Java versions. Which one does it currently run on?",并为 supportedVersions 中的每个条目提供一个选项(例如 8、17)。
    • 否则(javaSpecificationVersions 缺失/为空,或没有 mule-artifact.json)→ 提问 "I couldn't detect your app's current Java version. Which one does it run on today?"。根据当前运行时的 compatibleJDKs,从 anypoint-cli-v4 dx mule runtime list --output json 中得出选项(Mule 版本来自步骤 2a)——绝不使用硬编码选项。列表中未包含的那个运行时是 EOL 下限 4.3.x → 提供 Java 8/11 作为选项。如果用户无法提供版本,停止。

    然后通过使用 --user-version 重新运行脚本来持久化答案,以便后续步骤从文件中读取真实值(而不是 null)——在写回之前,答案只存在于本次对话中:

    node scripts/detect_current_java_version.mjs . --user-version <n>
    

这会重写 tmp/current-java-version.json,其中包含 version: <n>、source: "user-supplied",并在 <n> 低于下限(8)时设置 belowFloor: true——若出现这种情况,则按上述要求停止。

用户提供的版本也适用此下限。 重新运行 --user-version 时,脚本会为你应用下限(belowFloor: true)——若标记为低于下限,则此应用不在适用范围内:停止,并告知用户先将应用升级到至少 Java 8。

检测来源(由脚本实现):mule-artifact.json 的 javaSpecificationVersions——只有一个条目时使用该条目;有多个条目时询问用户选择哪一个;缺失/为空(或没有 mule-artifact.json)时询问用户。这是 Java 版本的唯一来源;不会将 pom.xml 编译器设置用作备用来源(它们是编译目标,而非部署时的运行时 Java 版本)。值会经过规范化处理(1.8 → 8)。


步骤 3:建立基线

在进行任何更改之前,确认应用在当前版本下可以构建。绿色基线是衡量后续每一步的参照点——如果应用现在无法构建,升级分析结果就没有意义。

3a. 确认当前版本(仅限检测到的值)

读取步骤 2 中的 tmp/current-mule-version.json 和 tmp/current-java-version.json。对于每个自动检测到的值(needsUserPrompt: false),在构建前与用户确认——绝不能使用自由文本的“是/否”提问。 使用 AskUserQuestion,让用户从具体选项中选择,而不是输入回复:

AskUserQuestion — 标题 Current versions,问题 "我检测到你的应用当前使用的版本为 Mule Runtime {muleVersion} 和 Java {javaVersion}。这些版本正确吗?确认后我就可以构建基线。" 选项(仅包含适用项——见下方条件规则):

  • 是,两者都正确 — 使用检测到的值继续构建基线。
  • 更正 Mule 版本 — 检测到的运行时版本有误;我会询问要使用哪个版本。
  • 更正 Java 版本 — 检测到的 Java 版本有误;我会询问要使用哪个版本。
  • 两者都更正 — 检测到的两个值都有误;我会先询问 Mule 版本,再询问 Java 版本。

根据步骤 2 中自动检测到的值的数量,决定展示哪些选项:

  • 两者都自动检测到 → 提供全部四个选项:是,两者都正确 / 更正 Mule 版本 / 更正 Java 版本 / 两者都更正。
  • 仅一个值自动检测到(另一个值由用户在步骤 2 中提供,因此已确认——不要再次询问)→ 针对单个检测到的值表述问题,并仅提供两个选项:是,正确 和 更正 {detected} 版本。省略其他“更正……”选项和 两者都更正。
  • 两者都由用户提供 → 完全跳过 3a。

用户选择任一“更正……”选项时(包括 两者都更正——逐个处理每个值),询问具体版本——若候选集已知,则通过 AskUserQuestion 询问(例如,提供 supportedVersions 中的 Java 主版本)——然后通过下方对应的 --user-version 重新运行命令,应用每项更正。

  • 仅确认检测到的值。用户已在步骤 2 中通过提示提供的值,已经确认——不要再次询问。 如果两个值都由用户提供,则完全跳过 3a。
  • 如果用户更正了某个值,后续都使用更正后的值,并像步骤 2 一样持久化,以便步骤 4 读取更正后的值(每次重新运行都会重新应用下限;若低于下限,则将 belowFloor 标记为 true):
    • 更正后的 Mule → node scripts/detect_current_mule_version.mjs . --user-version <v>
    • 更正后的 Java → node scripts/detect_current_java_version.mjs . --user-version <n>

此步骤得出的已确认当前 Java 主版本——即 tmp/current-java-version.json 中的 version 字段(当 2b 自动检测时),或用户提供/更正的值——即下方的 <current-java-major>。在 3b 和 3c 中使用同一值;如果此处已更正,切勿回退使用原始检测值。

3b. 确保当前 Java JDK 可用

基线必须能在应用当前使用的 Java 版本上构建,该版本可能与 $JAVA_HOME 当前指向的版本不同。使用步骤 3a 中已确认的当前 Java 主版本(<current-java-major>)运行辅助工具:

node scripts/resolve_jdk.mjs <current-java-major> .

它会写入 tmp/resolve-jdk-<major>.json。读取该文件并按以下情况处理:

  • available: true → 构建时使用 javaHome。该路径可能来自 Anypoint Code Builder java 目录下已安装的 JDK,也可能来自新下载的 JDK(source / downloaded 字段会说明来源)。
  • errors[] 非空(退出码 1) → 停止并报告错误。常见原因:未安装该主版本的 JDK,且无法解析构建字符串(CLI/DX 插件缺失或尚未通过身份验证)。

这是 Phase 2 用于目标 Java 的同一个辅助工具——每个所需的 Java 版本运行一次。

3c. 构建

首先,确保基线可在现代 Maven 上构建。 基线使用应用当前的 mule-maven-plugin(MMP)构建——即应用声明的版本(下文会检测,不会假定)。MMP 3.x 是基于 Maven 3.8 的 Eclipse Aether 构建的;Maven 3.9 用 Maven Resolver 替换了它,因此 3.x 插件在 Maven 3.9.x 上会于打包时因 NoClassDefFoundError: org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory 崩溃。工具链要求使用 Maven 3.9.x(已在步骤 1 中设为门槛——MMP 4.x 和 MUnit 都需要它);我们不会要求用户降级到已停止维护的 Maven 3.8。相反,如果当前 MMP 无法在 3.9.x 上运行,则仅为基线构建将其升级到最新的 4.x。

确定当前 MMP 是否需要升级才能构建基线:

node scripts/detect_current_mmp_version.mjs .

读取 tmp/current-mmp.json。如果 needsPluginBump: false(当前 MMP 已是 4.x)→ 直接开始构建。如果 needsPluginBump: true(当前 MMP 是 3.x,在 Maven 3.9.x 上会崩溃)→ 使用最新的 4.x MMP 建立基线。

从 Maven 元数据实时解析最新的 MMP(<release> 元素——这是权威的“最新已发布”版本;绝不硬编码版本)。将其缓存到 tmp/latest-mmp.txt,以便步骤 11a 复用此值,而不是重新获取相同的元数据:

MMP=$(curl -s "https://repository.mulesoft.org/nexus/content/repositories/releases/org/mule/tools/maven/mule-maven-plugin/maven-metadata.xml" \
  | grep -oE '<release>[^<]+</release>' | sed -E 's/<\/?release>//g')
echo "$MMP" > tmp/latest-mmp.txt
echo "latest MMP: $MMP"   # this is <latest-4.x> below

然后仅为基线构建应用此版本。根据 pom 固定插件版本的方式分为两种情况:

  • 情况 A——版本是属性(例如 <version>${mule.maven.plugin.version}</version>):在命令行中覆盖,不修改文件:
    JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<major>.json) \
      mvn clean package -Dmule.maven.plugin.version="$MMP"
    
  • 情况 B——版本是字面值(例如 <version>3.5.4</version>):-D 标志无法覆盖硬编码的插件版本,因此必须编辑 pom。不要使用 Edit/sed 手动编辑——带制表符缩进的 pom,以及多个 mule-maven-plugin 块(build/plugins 和 pluginManagement)容易导致错误。请使用确定性写入器,它仅重写匹配插件块中的 <version>,不受空白字符影响:
    cp pom.xml tmp/pom.baseline.bak
    node scripts/set_plugin_version.mjs mule-maven-plugin "$MMP" . --group-id org.mule.tools.maven
    JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<major>.json) mvn clean package
    cp tmp/pom.baseline.bak pom.xml   # ALWAYS revert — on success AND failure. The real bump happens in Phase 2 after approval.
    
    清楚记录:“当前 MMP 无法在 Maven 3.9.x 上运行;将使用 $MMP 构建基线(不会保留此更改——构建后会还原)。”

对于无需升级的 4.x MMP,只需使用已解析的 JAVA_HOME 运行构建(仅调用一次 mvn,响应中不包含其他内容):

JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<major>.json) mvn clean package

根据结果分支处理:

  • BUILD SUCCESS → 基线已建立(情况 B:确认 pom 已还原)。继续步骤 4。
  • 构建失败 → 停止。在情况 B 中,先还原 pom,再说明失败情况。告知用户,应用必须先使用当前版本(以及兼容的工具链)成功构建,之后才能继续升级。不要尝试通过升级修改来修复预先存在的基线构建失败。

步骤 3.5:Flow-XML 卫生扫描(Phase-1 门禁 — 每个 flow 文件)

此步骤的必要性。 mule-maven-plugin 3.x 的 process-classes 是空操作 — 它在构建时从不解析 flow XML。4.9+ 的 process-classes 会构建 Mule Runtime AST,并使用命名空间感知的 SAX 解析器解析每个 flow 文件,因此会拒绝任何前缀元素/属性(doc:name、<ee:transform>、db:config 等),只要其前缀未在作用域内的祖先元素上声明,并报错 The prefix "<p>" ... is not bound。此缺陷在 4.3 上可以正常发布,直到升级时才会显现 — 升级只是第一个会读取该 XML 的工具链。由于基线构建(步骤 3c)使用应用当前的(升级到 4.9 之前的)运行时,因此不会发现此问题。必须在此处于计划阶段,在每个 flow 文件中找出问题 — 而不只是后续步骤恰好要编辑的文件。(步骤 15.1 的逐文件门禁只会检查已编辑的文件;否则,未触及文件中的潜在前缀问题会一直留到步骤 16 的 mvn 才导致失败。此步骤正是为堵上这个漏洞。)

扫描每个 flow 文件。xmllint --noout 会将未绑定前缀报告为警告,并且仍以退出码 0 结束,因此检查退出码不能作为门禁 — 应改为检查其 stderr。这会解析真实 XML,因此不会像原始前缀正则表达式那样对 DataWeave 记号(accountId:)、错误类型值(VALIDATION:INVALID_VALUE)或其他 word: 文本产生误报:

hygiene_fail=0
for file in $(find src/main/mule -name '*.xml'); do
  errs="$(xmllint --noout "$file" 2>&1)"
  if printf '%s' "$errs" | grep -qE 'namespace error|not defined|not bound'; then
    echo "⚠️  $file — unbound namespace prefix (latent; fails on Mule 4.9+):"
    printf '%s\n' "$errs" | grep -E 'namespace error|not defined|not bound'
    hygiene_fail=1
  fi
done
[ "$hygiene_fail" = 1 ] && echo "→ record every file above in the plan's §Flow-XML hygiene section"
  • 干净(没有输出) → 没有潜在的未绑定前缀;继续执行步骤 4。
  • 标记出一个或多个文件 → 不要停止,也不要在此处编辑任何内容(Phase 1 不会写入项目文件)。记录每个受影响文件及其违规前缀,以便步骤 12 的计划列出修复项。错误文本会指出前缀;修复方法是在该文件的根 <mule> 元素中添加该前缀对应的 xmlns:<prefix>="<namespace-uri>" 绑定(从可正常工作的 flow 文件或连接器的 Mode-A .namespace 中查找 URI)。最常见的情况是存在 doc:name,但缺少 xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"。绝不要删除带前缀的属性/元素来消除错误 — 它们是元数据/功能,并非无用内容。

此处标记的每个文件都必须出现在步骤 12 的计划中,列在 §Flow-XML 卫生部分,作为明确的逐文件编辑项(add xmlns:<prefix>="<uri>" to root <mule>),包括计划其他部分未涉及的文件 — 这正是提前扫描的全部意义。步骤 15 随后会应用这些添加项,步骤 15.1 的逐文件门禁会再次验证。


步骤 4:确定目标版本

根据已确认的当前版本和实时运行时列表确定升级目标。绝不硬编码版本或通道 — 脚本会从 anypoint-cli-v4 dx mule runtime list 中推导所有信息。

运行解析器。它会从步骤 2 的 tmp/ 文件中读取当前版本(或接受 CURRENT_MULE / CURRENT_JAVA 覆盖值),并且 — 仅当用户已指定特定目标时 — 在你传入 TARGET_MULE(以及可选的 TARGET_JAVA)时进行验证:

# User has NOT named a target yet — just compute the recommendation:
node scripts/resolve_target_versions.mjs .

# User explicitly asked for a specific target (e.g. "move me to 4.11"):
TARGET_MULE=4.11 node scripts/resolve_target_versions.mjs .

目标是 Mule 版本。 TARGET_MULE 用于定义请求的目标 — 仅当用户指定了特定的 Mule 运行时时才传入。不要将 TARGET_MULE 设为当前版本来表达“保留 Mule”;这会被视为降级/无操作并遭到拒绝。

仅提及 Java(“将我的应用升级到 Java 17/11/8/21”)并不代表单独的目标 — 作为 Mule 升级的一部分,推荐方案始终会将 Java 升级到最新的非 EOL 版本。仅通过 TARGET_JAVA 传入他们指定的 Java(不传 TARGET_MULE),这样脚本就能检查它是否受推荐运行时支持:

# User mentioned only a Java version (e.g. "upgrade to Java 17"):
TARGET_JAVA=17 node scripts/resolve_target_versions.mjs .

然后始终展示 options[0](Mule + Java 路径),并读取 requestedJavaOnly:

  • requestedJavaOnly: null → 用户指定的 Java 就是我们推荐的版本(或者用户没有指定)。只展示推荐方案;无需额外说明。
  • 已设置 requestedJavaOnly → 用户指定的 Java 已 EOL(8/11)或不受推荐运行时支持(例如 21)。展示推荐方案,并且原样提供 requestedJavaOnly.note — 其中说明他们指定的 Java 不是受支持的目标,并指出我们支持的 Java 版本。

它会写入 tmp/target-versions.json。使用 jq 读取字段并进行分支处理:

  • needsUserPrompt: true → 脚本无法确定目标(没有当前版本,或无法获取运行时列表)。检查 warnings:获取失败意味着 CLI 尚未通过身份验证(anypoint-cli-v4 conf)或处于离线状态 — 向用户说明并重新运行。不要编造版本。
  • nothingToUpgrade: true → 应用已经使用所在通道中的最新运行时补丁版本,以及最新的非 EOL Java。告知用户无需升级并停止 — 不要继续执行阶段 2。
  • options[] 非空 → 这是推荐目标(始终在当前通道内:当前通道中的最高次要版本、最新补丁版本、最新的非 EOL Java)。目前恰好有一个条目。将其作为推荐方案展示。
4a. 展示推荐方案并确认用户的目标

options[0] 中的推荐方案是默认建议的目标。始终建议留在当前通道内,无论用户之后选择什么。向用户展示:

推荐升级:Mule {options[0].mule},Java {options[0].java}({kind})。{options[0].note,如有}

然后询问他们想选择推荐目标还是其他目标(Java + Mule,还是特定的 Mule 版本)。如果从他们最初的请求中无法明确选择,请使用 AskUserQuestion。

4b. 如果用户指定了具体目标,请验证该目标

当用户要求升级到推荐目标以外的版本时,使用 TARGET_MULE(如果用户指定了 Java,还要使用 TARGET_JAVA)重新运行脚本。从输出中读取 requestedTarget,并据此分支处理:

  • accepted: false → 该目标违反了锁定策略。原样说明 requestedTarget.reason,并重新提供推荐目标。不要手动绕过拒绝。reasonCode 可能为:

    • downgrade — 目标版本不严格高于当前版本(此技能只执行升级)。
    • eol-java — 目标保留或选择了已结束生命周期的 Java(8/11);此技能旨在帮助应用摆脱已结束生命周期的 Java。
    • unsupported-combo — 目标 Mule 不支持所请求的 Java。
    • unknown-version — 所请求的 Mule 不在运行时列表中。
  • accepted: true 且 crossChannel: false → 同一支持渠道内的目标(包括有效的中间版本,例如 4.4→4.6,或 4.4→最新 LTS)。通过 AskUserQuestion(而非自由文本)同时提供推荐目标和用户验证通过的目标,让用户选择——除非用户请求的目标已等于 options[0](Mule 和 Java 均相同),这种情况下无需选择,直接继续:

    AskUserQuestion — 标题 Upgrade target,问题 "你想升级到哪个目标版本?" 选项:

    • 推荐:Mule {options[0].mule},Java {options[0].java} — 同一支持渠道中的最新运行时。
    • 你请求的版本:Mule {requestedTarget.mule},Java {requestedTarget.java} — 你请求的目标已通过验证。

    当 requestedTarget.belowRecommended: true(用户请求的有效目标低于同一支持渠道中的最新运行时时),在请求目标选项的说明中原样展示 requestedTarget.note;该说明会指出用户的目标不是最新版本,并列出我们推荐的版本。根据用户的选择继续(如果用户坚持原选择,则使用 requestedTarget.mule / requestedTarget.java;否则使用 options[0])。

  • accepted: true 且 crossChannel: true → 目标切换支持渠道(LTS↔Edge)。这允许,但必须先发出警告。 原样展示 requestedTarget.warning,并通过 AskUserQuestion 获取明确确认(绝不能使用自由文本):

    ⚠️ {requestedTarget.warning}

    AskUserQuestion — 标题 Channel switch,问题 "此目标会切换支持渠道。你想如何继续?" 选项:

    • 继续切换渠道 — 继续执行步骤 5,目标为 Mule {requestedTarget.mule},Java {requestedTarget.java}(跨渠道)。
    • 保持推荐目标 — 回退到同一支持渠道内的推荐目标 Mule {options[0].mule},Java {options[0].java}。

    只有用户选择“继续切换渠道”后,才能使用跨渠道目标继续执行步骤 5。如果用户选择推荐目标,则回退到同一支持渠道内的推荐目标。

4c. 锁定目标

带入步骤 5 和阶段 2 的值为:目标 Mule 和 目标 Java——可以是已接受的推荐目标 options[0],也可以是经过验证并由用户确认的目标 requestedTarget。记录 Java 是否发生变化(javaChanged),以及之后是否需要修改父 POM。在用户确认一个明确的具体目标之前,不要继续。


步骤 5:提取连接器并检查当前 Java 兼容性

识别应用依赖的所有连接器,然后报告 Exchange 指明每个连接器当前锁定的版本支持哪些 Java 版本。用户会在计划中看到这些信息:正在使用哪些连接器,以及它们目前对 Java 的支持情况。确定与目标兼容的版本是后续步骤(步骤 6)的工作。

5a. 从 POM 中提取连接器

运行提取器。它会解析子级 pom.xml 及其完整的本地祖先链(父级、祖父级……),并收集所有带有 <classifier>mule-plugin</classifier> 且不属于 <scope>test</scope> 的 <dependency>。它会同等处理公共和自定义连接器(该 classifier 与发布者无关);如果同一个 groupId:artifactId 在多个层级中声明,则保留最近的声明(子级优先于父级,父级优先于祖父级——Maven 的“最近优先”规则)。

node scripts/extract_connectors.mjs .

它会写入 tmp/connectors.json。读取文件并根据结果分支:

  • connectors[] → 要检查的连接器。每项包含 groupId、artifactId、version、versionResolved、resolvedFrom(祖父级及以上为 "ancestor",父级为 "parent",子级为 "child");如果版本由祖先级管理(在祖先的 <dependencies> 中内联声明,或通过任何 <dependencyManagement> 管理),还会包含 versionManagedIn(管理该版本的祖先 POM 路径)。第 12 步会在计划的 Owned by 列中显示此信息,第 14/18 步会使用它:如果版本的所有者不是子级,就将版本更新交由 apply_parent_pom_fork.mjs 处理——第 14 步(--phase=edit)会将新版本写入该祖先 POM,第 18 步(--phase=fork)会更新祖先自身的 <version> 并重新指向子级,而不是编辑子级。仅当无法从任何本地 POM 解析出 version 时,它才为 null(见下文);第 5b 步会将无法解析的版本视为阻塞项。
  • excluded[] → 使用 test 作用域的 mule-plugin(MUnit 工具)。它们不是应用连接器;其版本稍后会与其他构建插件一同处理,而不是通过 Exchange 处理。不要在此处检查。
  • customLibraries[] → 应用中非连接器、非 test/provided、非平台的 jar(共享错误处理程序库、工具 jar、直接依赖的第三方依赖)。此技能不会升级它们(Exchange 中没有对应的目标版本),但它们可能是基于旧版 Java/运行时编译的。将它们带到第 12 步:它们会填充计划中的 Custom / non-connector libraries 部分,并触发主动的操作员确认提示(第 12.4 步)。不要在此处尝试解析或更新它们。
  • needsUserPrompt: true → 未找到连接器,或缺少子级 POM。检查 warnings[],并在继续前向用户确认。

版本解析方式(对本地 POM 链进行无需构建的静态解析——不使用 Maven,也不访问网络):

  • 内联的 <version>;
  • 来自任一本地 POM 的 <properties> 的 ${property}——单层(${http.version})、嵌套/链式(${a} → ${b} → 1.7.3)或组合(${major}.${minor}.${patch}、1.7.${patch}),其中后代属性会覆盖祖先属性;
  • 没有版本号的 <dependency>,其版本由本地 <dependencyManagement> 管理——可以是声明它的 POM 自身管理,也可以由链中任一祖先管理;
  • 在祖先 <dependencies> 中声明并由子级继承的连接器。

仅当本地 POM 中不存在该值时,version 才会保持为 null:父 POM 不在文件系统上(位于远程或 ~/.m2 中)、导入的 BOM(<scope>import</scope>)、<profiles> 块,或未知的 ${...},或引用形成循环。这些情况确实需要 Maven 的 effective model,且无法在本地编辑版本,因此会报告为无法解析,而不是猜测。声明了但缺失的父 POM 已在第 1 步中作为硬性阻塞项处理,所以不应到达此处;如果提取器仍对此发出警告,请停止并索取父 POM。

5b. 在 Exchange 中检查当前版本的 Java 兼容性

运行兼容性检查。对于每个连接器,检查会调用 anypoint-cli-v4 exchange asset describe <groupId>/<assetId>/<version>(精确查找,不使用模糊匹配的 asset list),并重试几次,以免将暂时的网络或身份验证故障误判为确实未找到,然后读取 is-java-*-supported 标签。

node scripts/check_connector_java_compat.mjs .

检查会写入 tmp/connector-java-compat.json,并在 stop: true 时以状态码 1 退出。读取文件并据此分支:

  • stop: false(退出状态码 0) → 所有连接器均已在本地解析 并 在 Exchange 上验证通过。可以继续。继续之前,用简短的成功提示告知用户,例如:

    ✅ 已在 Exchange 上解析并验证全部 {N} 个连接器。当前 Java 支持情况:http 1.7.3 → 8、11、17;db 1.13.5 → 8、11;……

    然后读取每个连接器的 connectors[].supportedJava,了解当前版本声明支持的 Java 主版本(例如 [8, 11, 17])。对于未阻止的连接器,如果 supportedJava 为空,表示标签存在,但值全为 false;请显示相应的 warnings[] 条目(该版本没有找到受支持的 Java 版本)。如果没有阻止项,继续执行第 6 步。

    • currentVersionDelisted: true(显示为 warnings[] 条目)→ 应用当前固定使用的版本已从 Exchange 下架(因此无法读取其 Java 兼容性标签),但该连接器仍可在 Exchange 上获取。这不构成阻止。请明确警告用户:该连接器的当前版本已从 Exchange 下架,因此无法验证其当前 Java 兼容性;然后继续(目标版本仍会正常验证)。

    只报告当前事实,不要在此得出有关目标版本的结论。说明每个当前版本目前支持什么,到此为止。不要与目标 Java/Mule 版本进行比较,不要说某个连接器“需要升级版本”或“仅支持 8/11,因此需要升级才能支持 17”,也不要提及任何目标版本。是否需要升级版本以及要升级到哪个版本,只能由第 6 步(resolve_target_connectors.mjs)决定;该脚本会获取同时支持目标 Java 和目标 Mule Runtime 的每个连接器的最新版本。在第 5b 步提前作出判断,会抢在脚本之前得出结论,可能导致错误猜测。

    • ✅ 允许:“db 1.13.5 当前支持 Java 8、11。”
    • ❌ 不允许:“db、file 和 sockets 仅支持 8/11——它们需要升级版本才能支持 Java 17。”
  • stop: true / blocked[] 非空(退出状态码 1) → 一个或多个连接器无法通过验证,升级无法继续。每个被阻止的连接器都包含一个 blockReason:

    • 在 Exchange 中未找到(describe 失败,且精确 GA 的 asset list 探测未找到匹配资源)——该连接器确实不在 Exchange 上:可能缺失、已发布的坐标不同、自定义连接器属于其他组织,或发生身份验证失败。此处包含原始 CLI 错误,可用于区分确实未找到和身份验证问题(anypoint-cli-v4 conf)。这与上述 currentVersionDelisted 不同(资源存在,只是旧版本已不存在)。
    • 没有 Java 兼容性信息——describe 成功,但资源没有 is-java-*-supported 标签,因此无法判断其 Java 支持情况。
    • 无法解析版本——连接器的版本是继承而来或未解析(参见第 5a 步),因此无法形成 Exchange 坐标。

    对于确实不存在的连接器(完全不在 Exchange 上),我们无法解析目标版本或验证兼容性。请询问用户是否仍要继续(保持当前固定版本,兼容性未知),还是停止。对于其他阻止原因,请说明情况并停止。问题解决之前,不要继续执行第 6 步或阶段 2。


第 6 步:解析与目标兼容的连接器版本

第 5 步报告了各连接器当前对 Java 的支持情况。本步骤将确定各连接器要升级到哪个版本:在目标 Mule Runtime 和 Java(来自第 4 步)上运行的最新已发布版本。这就是计划为每个连接器提出的新固定版本。

运行解析器。它会读取第 5a 步中的连接器以及第 4 步中的目标。

node scripts/resolve_target_connectors.mjs .

目标默认采用 tmp/target-versions.json 的 options[0](推荐目标)。若要针对其他目标进行解析——例如第 4 步中用户确认的 requestedTarget——请显式传入该确认的目标组合(使用 tmp/target-versions.json 中的值;切勿自行编造):

TARGET_MULE=<confirmed-mule> TARGET_JAVA=<confirmed-java> node scripts/resolve_target_connectors.mjs .

选择方式(每个连接器调用一次 Exchange)。 单次 exchange asset describe <groupId>/<assetId>/<currentVersion> 会返回 .versions[] 数组,其中列出所有同系列版本,每个版本都带有自己的 min-mule-version 和 is-java-<major>-supported 标签。因此,只需调用一次 describe,就能在本地筛选整个版本历史记录——无需对模糊的 asset list 进行分页,也无需逐个版本调用。它会保留同时满足以下两个条件的所有版本:

  • is-java-<targetJava>-supported == "true" — 该版本支持目标 Java;并且
  • min-mule-version <= targetMule — 该版本的运行时最低要求符合目标 Mule(没有 min-mule-version 标签的版本不符合条件)。

然后,它会按语义化版本选择符合条件的最高版本(“符合目标要求的最新版本”)。这会始终将每个连接器升级到最新的目标兼容版本——即使连接器当前固定的版本已经能在目标环境中运行,也会升级到符合要求的最新版本。只有当连接器当前版本就是最新的目标兼容版本(没有更高版本可升级)时,连接器才会保持不变,而不是仅仅因为它恰好兼容。

它会写入 tmp/target-connectors.json,并在 stop: true 时以状态码 1 退出。读取文件并据此分支处理:

  • stop: false(退出状态码 0) → 每个连接器都有目标兼容版本。向用户展示版本变更:每个 connectors[] 条目包含 currentVersion → targetVersion、changed(仅当当前版本已经是最新的目标兼容版本时才为 false,而非仅仅兼容)、minMuleVersion 和 supportedJava[]。这是升级计划中连接器相关的部分。继续第 7 步。
  • stop: true / blocked[] 非空(退出状态码 1) → 一个或多个连接器没有支持目标运行时的已发布版本。无法继续升级到该目标。每个受阻连接器都包含 blockReason(例如 没有已发布版本支持目标运行时(Mule X,Java Y))。向用户说明受阻连接器并停止。可选方案是选择其他目标(重新运行第 4 步 → 第 6 步),或等待连接器发布兼容版本——若有连接器无法解析,请勿继续进入第 2 阶段。

第 6.5 步:暂存下游数据契约(桥接)

第 7–20 步和第 2 阶段的变更脚本不会重新读取第 1–6 步各自的 tmp/*.json 文件。它们会读取本步骤根据现有输出写入的两个汇总契约。这是版本解析部分(第 1–6 步)与影响分析及执行部分(第 7–20 步)之间的衔接点。使用 Write(或 jq 构造)组装这两个契约,不要新建脚本——这里不需要 CLI,也不需要网络。

为什么需要重新映射。 第 5–6 步使用从制品 slug 派生的昵称为每个连接器设置键(mule-amazon-s3-connector → amazon-s3,mule-objectstore-connector → objectstore)。第 7/14 步则使用流程 XML 实际绑定的 XSD 前缀(xmlns:s3=… → s3,xmlns:os=… → os,xmlns:sfdc=… → sfdc)。第 7 步中,使用情况(connector-usage/<prefix>.json)与元数据/选项(…/<nick>-new.json)之间的关联要求字符串完全匹配——因此,本步骤写入的选项/目标文件必须以 XSD 前缀为键,而不是以 slug 为键。从 src/main/mule/**/*.xml 读取绑定(grep -ho 'xmlns:[a-zA-Z0-9_-]*=' src/main/mule/*.xml),并为 tmp/target-connectors.json 中的每个连接器,将其 groupId:artifactId 映射到命名空间 URI 与该连接器匹配的前缀(由 Agent 判断——与旧第 4.5 步分配昵称时使用的映射相同)。

已使用与仅在 pom 中 — 可根据同一次 xmlns 读取结果进行分类:

  • 已使用(连接器在 src/main/mule/ 下某处有 xmlns:<prefix> 绑定)→ 以该前缀为键。它会执行完整的第 7 步(Mode-A + 使用情况 + Mode-B/C)。
  • 仅在 pom 中(任何地方都没有绑定)→ 以其 slug 昵称为键。它只会得到一个选项文件——没有元数据,也不执行 describe。 它的版本仍会在第 2 阶段升级;其(不存在的)流程 XSD URL 会安全地空操作。
6.5a. 每个连接器的选项 — tmp/connector-choices/<nick>-new.json

对于 tmp/target-connectors.json 中的每个连接器,写入一个以其解析后的 <nick> 为键的文件(已使用的连接器用前缀,仅在 pom 中的连接器用 slug):

{ "groupId": "com.mulesoft.connectors", "assetId": "mule-amazon-s3-connector", "version": "<targetVersion>" }
  • groupId / assetId ← tmp/target-connectors.json 中该连接器的 groupId / artifactId(原样保留)。
  • version ← 其 targetVersion(目标固定版本——我们只描述新版,第 2 阶段也会固定到此版本)。

describe_connector.mjs <nick>-new 和 apply_connector_pin.mjs <nick> 都会读取此文件。不要为仅在 pom 中的连接器创建元数据占位文件——apply_connector_pin.mjs 会将 tmp/connector-metadata/<nick>-new.json 视为可选文件;若该文件不存在,则不会执行 XSD 重写。

6.5b. 升级目标约定 — tmp/upgrade-targets.json

apply_runtime_bump.mjs 会读取 .mule.to / .java.to;步骤 7/14 中的 .connectors[].nick 循环会遍历 .connectors[]。结构如下:

{
  "mule":            { "from": "<current>", "to": "<locked target>" },
  "java":            { "from": "<current>", "to": "<locked target>" },
  "muleMavenPlugin": { "to": "<latest MMP from Step 11a>" },
  "munit":           { "to": "<latest MUnit from Step 11a>" },
  "connectors": [
    { "nick": "s3", "groupId": "com.mulesoft.connectors", "artifactId": "mule-amazon-s3-connector", "from": "5.8.4" }
  ]
}

每个字段都要从已生成的来源中填写——绝不要硬编码版本号:

  • mule.from ← jq -r '.version' tmp/current-mule-version.json(步骤 2a),或用户在步骤 3a 中提供或更正的值。
  • java.from ← jq -r '.version' tmp/current-java-version.json(步骤 2b),或步骤 3a 中更正后的值。
  • mule.to / java.to ← 步骤 4c 中锁定的目标版本。从 tmp/target-versions.json 读取:接受推荐值 → .options[0].mule / .options[0].java;用户请求的目标通过验证并确认 → .requestedTarget.mule / .requestedTarget.java。使用用户确认的那组值——不要重新推导。
  • muleMavenPlugin.to ← 步骤 11a 中从 Maven 元数据实时解析出的最新 MMP。此时尚未知——步骤 11a 会在步骤 14 之前将其写回此文件。apply_runtime_bump.mjs 会读取它以升级 <mule.maven.plugin.version>;如果缺失,则保留该属性不变。
  • munit.to ← 步骤 11a 中从 Maven 元数据实时解析出的最新 MUnit。此时尚未知——步骤 11a 会在步骤 14 之前将其写回此文件。apply_runtime_bump.mjs 会读取它以升级所有 MUnit 版本位置(属性、插件以及 munit-runner / munit-tools 依赖);如果缺失,则保留这些位置不变。
  • connectors[] ← tmp/target-connectors.json 中每个连接器对应一个条目:nick 映射为 XSD 前缀(使用)或 slug(仅 pom),groupId / artifactId 原样填写,from ← 其 currentVersion。

写入后进行健全性检查:jq -e '.mule.to and .java.to and (.connectors|type=="array")' tmp/upgrade-targets.json。所有后续步骤都会读取 mule.to / java.to 并遍历 .connectors[]——如果任一目标为 null 或缺少 connectors,请在继续步骤 7 之前在此处修正。


步骤 7:检查操作、配置和错误类型变更

从新版连接器中查明操作、配置和错误类型有哪些变化,以便步骤 12 能综合出按符号划分的机械式升级计划。此流程包含四种 describe 模式:7a Mode-A 摘要、7b 用法枚举(旧版侧的事实来源——扫描 flow-XML,而不是再次执行 describe)、7b Mode-B 按操作、以及 7b Mode-C 按配置提供方。下文内联说明了每种模式的输出格式。

前置条件:Java 17+(在任何 describe_connector.mjs 调用之前)。 为运行此步骤的 shell 导出 Java 17 JAVA_HOME(如何解析请参阅步骤 13)。该脚本会拒绝在 Java 8/11 下运行,因为这些 JDK 会返回降级的 describe 结果(configs[].parameters 为空),从而悄然漏掉必需属性的破坏性变更。如果步骤 3b 只解析出了当前(低于 17)的 JDK,请立即用相同的辅助工具解析 Java 17 JDK,并在 Mode-B / Mode-C 扇出之前重新验证 $JAVA_HOME(子 shell 或 cd 可能会重置它):

node <skill-dir>/scripts/resolve_jdk.mjs 17 .
export JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-17.json)

前置条件:已注册可用于 describe 的 Mule ≥ 4.9 运行时。 describe-connector 会通过本地安装的 Mule Runtime 加载连接器捆绑的 poms,与应用的目标运行时无关(应用仍可能以 4.6 为部署目标)。低于 4.9 的运行时会因 CLI 插件调用 mule-runtime-ast.ErrorTypeBuilder.builder() 而静默失败,并显示 Java exited with code 1;该静态方法仅在 Mule 4.9+ 中存在。请确保磁盘上有 ≥ 4.9 的运行时,然后向 CLI 注册其路径(这是一次性的全局 CLI 配置写入;resolve 脚本本身不会设置该路径)。如果 mule.to ≥ 4.9,则复用已确定的目标版本,否则使用 4.9+ 基础运行时:

# Pick a >=4.9 substrate: reuse mule.to when it already qualifies, else 4.9.19.
SUBSTRATE=$(jq -r 'if (.mule.to | split(".") | map(tonumber)) as $v | ($v[0] > 4 or ($v[0] == 4 and $v[1] >= 9)) then .mule.to else "4.9.19" end' tmp/upgrade-targets.json)
node <skill-dir>/scripts/resolve_runtime.mjs "$SUBSTRATE" .
anypoint-cli-v4 dx mule runtime path --set "$(jq -r .runtimePath tmp/resolve-runtime-$SUBSTRATE.json)"

如果之后 describe 仍然静默失败,可使用 _JAVA_OPTIONS='-Xlog:exceptions*=info:file=/tmp/xlog.log' node <skill-dir>/scripts/describe_connector.mjs <nick>-new 捕获隐藏异常,然后运行 grep NoSuchMethodError /tmp/xlog.log;如果匹配到 ErrorTypeBuilder.builder(),则可确认已注册的路径仍低于 4.9。

7a. Mode-A 摘要描述(每个连接器在步骤 6 中固定到的 NEW 版本)

对于 tmp/upgrade-targets.json 中每个已使用的连接器昵称 <nick>(跳过 pom-only——它们没有可供检查的流程用法):

node <skill-dir>/scripts/describe_connector.mjs <nick>-new

昵称规范(阻塞项)。 <nick> 必须与流程 XML 使用的 XSD 前缀相同(crypto、os、xml-module、saml),而不是工件标识(cryptography、objectstore、xml)。步骤 6.5 已将 choices/targets 文件重新映射为前缀;请在 Mode-A → Mode-B → Mode-C 之间保持一致。enumerate_usage.mjs 仍会通过扫描每个 *-new.json 中的 .namespace.prefix == <nick> 来解析不匹配的昵称,但依赖此回退意味着每个下游脚本也必须使用正确的文件名调用——直接使用前缀更省事。

继续之前,请确认 tmp/connector-metadata/<nick>-new.json 存在,并且其 .namespace 是一个 .prefix 非空的对象。describe_connector.mjs 不会保存 .namespace 为裸字符串的 Mode-A 文件——如果 CLI describe 被阻止(受授权限制的连接器),而你正在手动编写元数据,请遵循对象格式 {"prefix": "...", "namespace": "...", "schemaLocation": "..."},否则下面的用法提取器会因 jq 索引错误而退出。

describe_connector.mjs 会传递 -Dmule.jvm.version.extension.enforcement=LOOSE,因此即使新连接器的扩展模型声明 supportedJavaVersions=[1.8, 11],它仍可在 Java 17 下完成描述。

写入 tmp/connector-metadata/<nick>-new.json——顶层摘要。Mode-A JSON 格式:

  • operations[] — 顶层操作名称(字符串)
  • sources[] — 顶层源名称
  • configs[] — 每项为 {name, connectionProviders: [...bare strings...]}。摘要中的提供程序是裸字符串;DSL .elementName 仅在 Mode-C 输出中填充(见 7b)。
  • errorTypes[] — 连接器级错误类型并集
  • supportedJavaVersions[] — 声明的 Java 兼容版本范围

摘要不包含每个操作的属性列表,也不包含每个配置提供程序的 DSL 元素名称——这些信息来自 7b 中的 Mode-B(每个操作)和 Mode-C(每个配置提供程序)。

Mode-A ≠ Mode-B——不要在 Mode-A 中搜索属性名称。 摘要只列出 .operations[].name 和 .configs[].name;不包含 .operations[<op>].attributes[] 或 .childElements[]。属性重命名、必需属性新增以及属性→子元素提升,只能在 Mode-B(<nick>-new-<op>.json)中看到。基于 Mode-A 构建计划中的逐操作属性差异,会悄然漏掉破坏 XSD 兼容性的变更——随后构建会在 process-classes 阶段因 cvc-complex-type.3.2.2 错误失败,而这些错误本可以在计划阶段发现。如果需要属性信息,请对该操作运行 Mode-B。

只描述 NEW 版本。 不要描述 OLD 连接器版本——OLD 端的事实来源是下方的 enumerate_usage.mjs(流程 XML 扫描),而不是再次执行 describe。4.6 之前的连接器通常无法在 Java 17 JDK 下完成描述;本技能的设计不依赖 OLD describe。参见 feedback_upgrade_describe_new_only。

7b. 用法枚举与逐符号扇出

用法枚举——优先使用解析器,失败时回退到 grep。 两个可互换的脚本会写入相同格式的 tmp/connector-usage/<nick>.json:

  • enumerate_usage_xml.mjs — 使用 fast-xml-parser 解析每个流程。能正确处理杂乱输入:忽略已注释的元素,并将 config-ref 关联到实际携带它的元素。优先使用。
  • enumerate_usage.mjs — 零依赖的正则/grep。始终可用;作为回退方案。 该技能无状态,因此请临时安装解析器、运行它,然后将其移除。fast-xml-parser 会安装到最近的包根目录 skills/mule-development/node_modules(已加入 gitignore;--no-save 不会改动 package.json)。将该根目录解析为其物理路径(pwd -P),以确保包安装到 Node 的 ESM 解析器会查找的位置:Node 会从脚本的实际路径向上遍历,因此对文本形式的 <skill-dir>/.. 使用 --prefix 可能会将包安装到 Node 永远不会搜索的位置,导致每次解析器调用都回退到 grep。对每个范围内的连接器都按如下方式运行枚举:
SKILL_PKG="$(dirname "$(cd "<skill-dir>" && pwd -P)")"   # skills/mule-development (physical package root)
npm install --no-save --prefix "$SKILL_PKG" fast-xml-parser >/dev/null 2>&1 || true

for nick in $(jq -r '.connectors[].nick' tmp/upgrade-targets.json); do
  # Prefer the parser; rc=3 means fast-xml-parser wasn't importable → grep fallback.
  # Capture the exit code into a named var immediately (portable across bash/zsh;
  # a bare `[ $? -eq 3 ]` misfires if anything runs between the call and the test).
  node <skill-dir>/scripts/enumerate_usage_xml.mjs "$nick" .
  rc=$?
  [ "$rc" = 3 ] && node <skill-dir>/scripts/enumerate_usage.mjs "$nick" .
done

临时的 node_modules(fast-xml-parser 的安装依赖闭包)会在步骤 19 中移除。如果 npm install 被阻止(离线/受限环境),每次解析器调用都会以 rc=3 退出,grep 回退机制将接管整个步骤,无需人工干预。

用法 JSON 结构——两个脚本都会写入相同的 tmp/connector-usage/<nick>.json:

  • operations_used[] — 被分类为 NEW 端操作或元数据未知的元素名称
  • sources_used[]、configs_used[]、config_providers_used[]
  • child_elements_used[] — 被分类为 <prefix:config> 的已知子元素或元数据未知的内联子元素(例如 content、objectContent、records)的元素名称。此项会明确列出,避免将 grep 噪声误读为“流使用了此操作”。
  • usage_sites[] — 按使用位置列出的 {file, line, attributes_set}。会过滤掉 doc:name。
  • errorTypes_caught[]、errorTypes_raised[]
  • namespace_prefix — 流使用的 DSL 前缀(例如 <s3:create-object .../> 中的 s3)
  • namespace_prefix_changed — 当 NEW 前缀与流使用的前缀不同时为 {from, to}(例如 SFDC 的 sfdc → salesforce),否则为 null

前缀回退规则。 如果任何流 XML 中都没有出现 NEW 元数据前缀作为元素开标签,脚本会在流中查找与之绑定相同命名空间 URI 的其他 xmlns:<candidate>="<same namespace URI>",并使用该候选前缀重新运行 grep。

not_in_use 跳过约定。 如果 enumerate_usage.mjs 为某个连接器返回 not_in_use JSON(该连接器在 pom.xml 中声明,但流中使用次数为零),则跳过该连接器的 Mode-B 和 Mode-C。将它保留在计划的 pom-only 部分中,以便 Phase 2 仍运行 pin 脚本。不要臆造任何其他 Mode-B/C 之前的短路条件——对于每个有实际用量的连接器,都必须无条件运行 Mode-B/Mode-C;当与 Mode-B/Mode-C JSON 的逐符号差异为空时,“无需重写”的结论会在步骤 12 的计划综合中自然得出。

Mode-B 的存在原因。 Mode-A 摘要只返回顶层操作名称,不包含属性列表或子元素。没有每个操作的详细信息,计划就只能猜测 NEW 端的属性重命名(例如 bucketName → bucket)以及子元素与属性的位置区别(例如,在 NEW putObject 中,<s3:content> 是 childElement,而非属性)——这些猜测只有到 mvn 时才会暴露。Mode-B 输出结构——每个 tmp/connector-metadata/<nick>-new-<op>.json 都包含:

  • attributes[] — NEW 操作接受的所有属性,以 .attributeName 为键(不是 .name),并包含类型、required 标志和 allowedValues
  • childElements[] — NEW 操作接受的所有子元素(名称、前缀、是否必需、属性)
  • errorTypes[] — 每个操作的错误目录
  • .output* 键 — 当操作声明了输出类型时填充;供步骤 9 的 DW 差异分析使用

调用 Mode-B 之前,求 usage.operations_used[] 与 <nick>-new.json .operations[] 的交集:

  • .operations[] 中存在该操作 → 对其运行 Mode-B。
  • .operations[] 中不存在该操作 → 该操作已重命名或移除。选择最接近的重命名候选项(Levenshtein 距离较近或语义角色相同——例如 S3 8.x:createObject → putObject,readObject → getObject),并对该候选项运行 Mode-B。将猜测的重命名记录到 tmp/connector-metadata/<nick>-op-renames.json,以便 Step 12 的计划明确列出。绝不可悄悄跳过操作——流 XML 仍会调用它。Mode-B 调用返回非零状态或空结果,本身就是强烈的重命名信号(该操作可能已不再使用此名称)——请与 .operations[] 交叉核对,并对候选项重新运行。

具体调用方式——使用标志,不使用位置参数。 文件顶部的随附脚本表列出了语法;此处重复说明,以免子 Agent 需要滚动查找:

# Mode-B — per operation or source
node <skill-dir>/scripts/describe_connector.mjs <nick>-new --type operation --name <op>
node <skill-dir>/scripts/describe_connector.mjs <nick>-new --type source    --name <src>

# Mode-C — per config-provider (both --name and --config-name are required)
node <skill-dir>/scripts/describe_connector.mjs <nick>-new --type connection-provider --name <provider> --config-name <config>

将操作名称作为位置参数传入(describe_connector.mjs <nick>-new <op>)不受支持,并会触发“参数缺失/不完整”退出——2026 年 7 月的一次真实运行因试错标志顺序,浪费了 2–4 次工具调用。

Mode-C 的存在原因,以及 --name / --config-name 的使用规范(阻塞项)。 <prefix:config> 内的连接元素(例如 <s3:basic-connection>、<jms:active-mq-connection>)无法从摘要推导出来——Mode-A 只报告提供方的 SDK 标识符,不报告其 DSL 元素名称。--config-name 和 --name 是连接器在 SDK 侧的标识符,取自 Mode-A 的 <nick>-new.json,绝不可取自流 XML:

  • --config-name ← .configs[].name(单个小写词——通常就是 "config" 或 "listenerConfig")
  • --name ← .configs[].connectionProviders[] 条目(单个小写词——例如 "oracle"、"connection"、"active-mq"、"listener")

不要传入:用户的 XML 配置标识符(Warehouse_DB_Config——这些应在调用位置放入 config-ref);旧 DSL 提供方元素名称(basic-connection、oracle-connection——这些是 Mode-C 返回的 .elementName,不是传入的值);或 tmp/connector-usage/<nick>.json 中的任何 configs_used[] / config_providers_used[](这些内容来自旧流 XML)。Mode-C 输出结构——每个 tmp/connector-metadata/<nick>-new-<config>-<provider>.json 包含:

  • .elementName — 配置元素名称(例如 sfdc-config)
  • .connectionProviders[] — 此配置上的连接提供方;每个提供方包含 elementName(例如 basic-connection)、attributes[] 和 childElements[]

编写连接元素改写时,使用此 Mode-C 文件中的 elementName——不要根据 SDK 提供方名称猜测,也不要读取 Mode-A 中的 .elementName(其中没有此字段)。

强制扇出循环——每个操作运行一次 Mode-B,每个提供方运行一次 Mode-C,不得例外。 不要“每个连接器抽查一个操作”——tmp/connector-usage/<nick>.json 中的每个操作 / source / 提供方,在 Step 11.5 之前都必须有自己的 describe 文件。跳过扇出会让 Step 12 无法发现属性重命名和必需属性新增;随后 Step 16 的重试循环就会耗尽全部预算来猜测。

对每个连接器执行以下循环来完成扇出(逐字粘贴——不要在内联代码中重新实现)。该循环具有幂等性——已存在的 <nick>-new-<op>.json 会被跳过,因此在部分运行后重新执行成本很低:

for usage in tmp/connector-usage/*.json; do
    nick="$(basename "$usage" .json)"
    usage_status="$(jq -r '.status // ""' "$usage")"   # NOT `status` — read-only in zsh
    [ "$usage_status" = "not_in_use" ] && continue

    modeA="tmp/connector-metadata/${nick}-new.json"
    [ -f "$modeA" ] || { echo "❌ Mode-A missing for $nick — re-run Step 7a"; exit 1; }

    # Mode-B per operation (intersect with Mode-A .operations[])
    for op in $(jq -r '.operations_used[]? // empty' "$usage"); do
        known="$(jq -r --arg n "$op" '[.operations[]? | if type == "string" then . else .name end] | index($n) // "none"' "$modeA")"
        if [ "$known" = "none" ]; then
            echo "⚠️  $nick/$op — op absent from Mode-A .operations[] (rename/removed); Step 12 must consult <nick>-op-renames.json"
            continue
        fi
        out="tmp/connector-metadata/${nick}-new-${op}.json"
        [ -f "$out" ] && continue
        node <skill-dir>/scripts/describe_connector.mjs "${nick}-new" --type operation --name "$op"
    done

    # Mode-B per source
    for src in $(jq -r '.sources_used[]? // empty' "$usage"); do
        out="tmp/connector-metadata/${nick}-new-${src}.json"
        [ -f "$out" ] && continue
        node <skill-dir>/scripts/describe_connector.mjs "${nick}-new" --type source --name "$src"
    done

    # Mode-C per (config, provider) — driven from Mode-A .configs[], per the
    # "--name / --config-name discipline" block above.
    # Do NOT drive from usage.configs_used[] / config_providers_used[]: those
    # hold flow-instance names (config-ref values like db-config-primary, and
    # camelCase child names like genericConnection) that never equal Mode-A's
    # SDK names (config, generic) — the old join matched nothing and silently
    # wrote zero Mode-C files, so Phase C never saw reparenting like db's
    # <pooling-profile> and the first mvn broke on XSD validation.
    # --config-name ← .configs[].name; --name ← .configs[].connectionProviders[]
    # entry. Configs with an empty connectionProviders[] are skipped (D7
    # fallback — Phase C reads Mode-A .configs[] directly there).
    jq -r '.configs[]? as $cfg
             | $cfg.connectionProviders[]?
             | "\($cfg.name)\t\(if type == "string" then . else (.name // .elementName) end)"' "$modeA" \
      | while IFS=$'\t' read -r cfg prov; do
        [ -z "$cfg" ] && continue
        [ -z "$prov" ] && continue
        out="tmp/connector-metadata/${nick}-new-${cfg}-${prov}.json"
        [ -f "$out" ] && continue
        node <skill-dir>/scripts/describe_connector.mjs "${nick}-new" --type connection-provider --name "$prov" --config-name "$cfg"
    done
done

后置条件(自检)——Step 7 声明“完成”前必须通过。 不要拖到 Step 11.5 再做:

node <skill-dir>/scripts/verify_metadata_coverage.mjs || { echo "❌ Mode-B/C fan-out incomplete — re-run the loop above until coverage passes"; exit 1; }

如果 verify_metadata_coverage.mjs 打印出 FAIL 行,请仅对这些 (nick, op) / (nick, cfg, prov) 配对重新运行扇出循环。该循环具有幂等性——只有目标文件缺失时才会重新调用 describe。

Mode-B / Mode-C 完成后,运行以下强制差异检查(这些结果将用于 Step 12 的 Phase-C 完整性清单):

  • Mode-B .attributes[].attributeName(不是 .name)与 usage.usage_sites[].attributes_set 键 → 属性重命名
  • usage.errorTypes_caught[] 与 Mode-B .errorTypes[] ∪ Mode-A .errorTypes[] → 错误类型重命名(必须处理,不可择机处理——将这些问题推迟到构建时自我纠正会消耗重试额度)
  • Mode-C .connectionProviders[].elementName(该连接器的完整集合)与旧流程的 <prefix:config> 连接元素本地名称 → provider 元素重命名。 这里改变的是 provider 元素本身的名称,而非其子树——若 provider 的子元素未变化,下方的子树条目不会发现任何差异,因此无法捕获此变更。测试 = 大小写归一化后的集合成员检查(Mode-C 的 elementName 为 kebab-case,usage.config_providers_used[] 为 camelCase——比较前将二者统一转换为同一种形式,否则未重命名的连接器也会产生误报):如果旧本地名称不在该连接器新的 elementName 并集内,则说明 provider 已重命名 → 计划将 <prefix:config> 子元素重写为仍有效的元素。漏掉 provider 重命名必然会导致 process-classes XSD 失败。
  • Mode-C 子树差异 — 递归,覆盖两个层级:
    • .childElements[](配置层级,递归遍历每个嵌套的 .childElements[] / .containedElements[])与旧流程配置子元素树
    • .connectionProviders[].childElements[](provider 层级,递归遍历)与旧流程 provider 子元素树 可捕获配置 ↔ provider 之间的重新归属(例如,mule-db-connector 1.16.x 将 <db:pooling-profile> 从 <db:config> 子元素移至 <db:oracle-connection> 子元素),也可捕获嵌套结构差异,例如 <vm:queues><vm:queue …/></vm:queues>,其中整个子树位于配置层级的子元素下,而非 provider 下。不要只检查顶层名称——两层深处的重命名或结构调整会被漏掉。

每个差异都必须作为明确的逐符号编辑,列入第 12 步的计划(tmp/upgrade-plan.md)。

  • 描述版本变更对应的连接器操作
  • 识别操作、配置和错误类型的变更
  • 标记对流程和配置组件的影响

第 8 步:检查自定义 Java 兼容性

没有脚本,这里也没有任何确定性操作。 与连接器步骤(Mode-A/B/C 元数据 → 按符号机械重写)甚至 DataWeave(第 9 步的固定模式目录)不同,Mule 应用中的自定义 Java 没有机器可读的契约,也没有标准修复方案。没有 describe,没有 XSD,也没有可供比较差异的 tmp/*.json。在计划综合阶段(第 12 步),Agent 会使用 Read 工具直接读取应用自己的 Java 源代码,并根据自身对 Java 迁移的了解推理每个位置。此步骤的标准是功能正确性:升级后的应用调用自定义 Java 类时,必须仍能编译,并且在运行时行为保持一致,而不只是成功执行 mvn package。

仅当升级跨越 Java 版本边界时才运行此步骤——这是这里唯一确定性的门槛。 将 currentJava(tmp/current-java-version.json .version)与 targetJava(第 4 步确认的目标版本)进行比较。如果 targetJava == currentJava(例如,仅升级 Mule 运行时,而应用仍使用相同的 Java 版本),则没有跨越 Java 版本边界,因此不可能出现由版本引入的破坏性变更——在计划中记录“已跳过 — Java 未变(<currentJava> → <targetJava>)”,然后继续。只有当 targetJava > currentJava 时,才执行下面的 FIX/FLAG 分析。(此技能绝不会降级。)

只标记 currentJava 与 targetJava 之间实际发生变化的内容。 下列每项破坏性变更都对应引入它的 Java 版本——只有当该版本位于源版本→目标版本的跨度内时,才标记命中项。这样,无论版本跨度如何,都无需硬编码上限:从 8 升级到 17 会跨越 Java 11 的移除项以及 16/17 的封装变更;从 11 升级到 17 则只跨越后者;而目标版本之后引入的变更(例如 Java 18 将默认编码切换为 UTF-8)不在跨度内,不得标记。不要追查那些与已知版本中具体移除、封装或弃用的 API 无关的泛泛“行为可能漂移”问题。

为什么构建成功还不够。 mvn 只能捕获(下文列出的)源代码编译层面的问题。大多数自定义 Java 破坏性问题——反射因模块封装而受阻、Spring bean 无法实例化、序列化对象图无法再往返、某个安全提供程序已被移除——只会在部署或运行时出现,而 -DskipTests 构建根本不会触发这些问题。因此,此步骤分为两种处置方式:

  • 修复 — Agent 能够读取并重写的部分。在计划中提出具体修改。
  • 标记 — 此技能既无法验证,也无法安全自动修改的仅运行时问题。将其记录为需要操作员关注的事项,以便用户在审批环节了解:构建成功并不能证明这些内容安全。

范围 — 仅限应用自身的源代码和流触点,不包括不透明 JAR。 第 5 步中的不透明 customLibraries[] JAR 是预编译二进制文件,技能无法读取或修复;它们已有独立的操作员确认流程(第 5a 步 → 计划 §自定义/非连接器库 → 第 12.4 步)。不要在这里重新检查它们,也不要对它们作出兼容性判断。

首先,查明现有内容。如果没有自定义 Java 源代码,也没有 <java:*> / java! / <spring:bean> 用法,则此步骤无需执行——在计划中记录“无 — 应用中没有自定义 Java”,然后继续:

find src/main/java src/test/java -name '*.java' 2>/dev/null
grep -rlE '<java:(invoke|new|invoke-static)|java!|<spring:bean' src/main/mule src/main/resources 2>/dev/null
8a. 修复界面——阅读源代码并提出修改

阅读 src/main/java/** 和 src/test/java/** 下的每个 .java 文件(测试 Java 代码会像主 Java 代码一样,导致第 16 步构建失败,也会导致第 17 步 MUnit 运行失败),以及通过 <java:invoke> / <java:new> / <java:invoke-static> 从流中调用的类和通过 DataWeave java! 调用应用类的代码。以下三类问题确实会影响 Mule 应用——只有当其版本落在 currentJava→targetJava 范围内时才标记:

  1. 已移除的 JDK / Java EE 模块(JEP 320,在 Java 11 中移除)——import javax.xml.bind.*(JAXB)、javax.xml.ws.*/javax.jws.*(JAX-WS)、javax.activation.*、javax.annotation.*、javax.xml.soap.*、javax.transaction.*、CORBA(org.omg.*)。在 Java 8 上可编译,在 Java 11 及更高版本上会因 package … does not exist 而失败。修复 = 添加独立的 jakarta.* 依赖(或迁移命名空间)。在计划中记录准确的坐标——添加依赖是第 15.4 步的修改。
  2. 受限的 JDK 内部 API(JEP 396/403,在 Java 16/17 中封装)——sun.*、com.sun.* 内部 API、jdk.internal.*(sun.misc.Unsafe、BASE64Encoder、sun.security.*)。可能可以编译,但会在运行时抛出 InaccessibleObjectException。优先使用公开替代方案(BASE64Encoder → java.util.Base64,Unsafe → VarHandle);只有在没有替代方案时,才记录 --add-opens/--add-exports(运行时配置,不是 pom.xml)。
  3. 默认区域设置(JEP 252,Java 9 及更高版本)——未指定显式 Locale 的 SimpleDateFormat / DateTimeFormatter / NumberFormat 会采用 CLDR 数据;月份/星期名称和分隔符会发生变化。固定 Locale。(相关的默认 UTF-8 切换 JEP 400 在 Java 18 中生效——仅当 targetJava >= 18 时才标记。)
  4. 已移除 / 已弃用且计划移除的方法——Class.newInstance() → getDeclaredConstructor().newInstance();装箱基本类型的构造函数 new Integer(…) → Integer.valueOf(…);Thread.stop/Thread.destroy、Runtime/System.runFinalizersOnExit(),以及已移除的 SecurityManager.check* 变体 → 文档中说明的替代方案。这些问题大多只会产生编译警告,而不会在 Java 17 或更低版本中直接失败,因此优先级低于 (1)/(2);仍需记录。

这是一份备忘清单,不是许可清单——应考虑文件中使用的任何在源 Java 版本与目标 Java 版本之间发生变化的 API,而不只是上述四类。对于每个问题,在第 12 步的计划中记录:file:line · 有问题的代码行 · 类别(已移除的包 / 受限的内部 API / 已弃用的方法 / 区域设置) · 建议的修复方法。没有阅读上述 find/grep 找到的文件之前,绝不能声称“无自定义 Java 影响”。

8b. FLAG 表面 — 仅运行时,须由操作员确认

这些情况涉及自定义 Java,但只会在应用部署或运行时失败(如果会失败)——编译通过且使用 -DskipTests 的构建无法发现它们,技能也无法验证它们。在计划中将每个情况记录为一个已知风险/需操作员关注条目,注明 file:line 和可能导致故障的原因——不要自动编辑:

  • 反射访问 JDK 模块 — 对 java.* 成员调用 setAccessible(true),跨模块访问 Field/Method:即使编译通过,在 Java 16/17+ 上运行时仍会抛出 InaccessibleObjectException。注明应用所需的 --add-opens / --add-exports(这些参数应配置在应用的启动/运行时配置中,而不是 pom.xml)。
  • 按类实例化的 Spring bean — src/main/resources/** 和导入的 bean 文件中的 <spring:bean class="…">:该类必须能在目标 Java 上加载并完成构造;若其构造函数触发 8a 中的某种模式,则会在上下文启动时失败。
  • 跨边界传递的可序列化状态 — 持久化到 Object Store、放入 VM 队列或在集群中复制的对象:序列化图引用了已移除的 javax.* 类型,或 serialVersionUID 发生变化,都可能导致升级后无法反序列化已持久化的数据。
  • 自定义安全/加密/TLS Java 代码 — 自定义 java.security.Provider、硬编码的算法/提供程序名称、自定义 KeyStore/TrustManager:Java 17 移除并重新排序了提供程序,还禁用了弱算法;行为可能悄然变化。
  • 自定义日志附加器 — log4j2.xml 引用的 Java log4j2 附加器类:以旧版 Java 编译,并在启动时加载。

此步骤仅用于发现——阅读并标记,但不要进行任何编辑(Phase 1 不会写入项目文件)。8a 的发现归入计划中的“自定义 Java 下游影响”部分;8b 的发现归入计划中的“已知风险/需操作员关注的事项”部分。实际的 8a 源码改写和依赖添加将在 Step 15.4 中进行;8b 项目将在 Step 12.4 关卡提交给操作员。

  • 在应用中识别自定义 Java(src/main/java、src/test/java、<java:*> 流调用、java!、Spring bean)
  • 标记与目标 Java 版本可能不兼容的情况——修复可读的源代码,标记仅运行时的表面

步骤 9:检查 DataWeave 兼容性

此步骤无需脚本。 Agent 会在计划综合阶段(步骤 12)使用 Read 工具直接读取 DW 源码。将符号与 tmp/connector-metadata/<nick>-new-<op>.json 中 Mode-B 的 .output* 键进行比较:

  • 符号存在于 Mode-B 中 → 无需更改
  • 符号不存在,但存在同级项 → 在计划中提出重写方案
  • 符号不存在,且 Mode-B 没有 .output* 键 → 标记为 SITE FLAGGED FOR OPERATOR

需要读取的来源:

  • src/main/mule/**/*.xml 下的每个 <ee:transform> 块
  • src/main/mule/**/*.xml 下的每个内联 #[...] 表达式

Java 17 升级模式 — 检查每个 DW 文件(内联文件以及 src/main/resources/** 下的 .dwl 文件)是否存在以下八种情况,并将每个命中项添加到计划的“DataWeave 下游影响”部分:

  1. 对外部字符串使用 as Number / as Integer — Java 17 的 NumberFormat 会拒绝 Java 8 可接受的千位分隔符和空白字符。转换前使用 sanitizeNumeric() 辅助函数(去掉 , 并修剪空白)。
  2. sizeOf(x as Object) / keysOf(x as Object) / 对 Map 或 Array 使用任何 as Object — as Object 转换原本用于将值包装为不透明对象;Java 17 + 较新版本的 DW 会拒绝此用法。移除该转换。
  3. now() + <Number> / <DateTime> + <Number> — 隐式的 Number→Period 强制转换已不再支持。转换为显式的时间段字面量,例如 now() + |P7D| 或 now() + |PT1H|。
  4. formatDate(x, pattern) / parseDate(s, pattern) 未指定 {locale: ...} — JEP 252 将默认区域设置提供程序切换为 CLDR;月份和星期名称可能发生变化。显式添加 {locale: "en-US"}(或应用的标准区域设置)。
  5. 硬编码依赖 Charset.defaultCharset(),或读写文件时未指定字符集 — JEP 400 将 JVM 默认字符集切换为 UTF-8。每次读写都明确指定字符集。
  6. dw::Runtime::run(..., engine: "javascript") 或任何 Nashorn 调用 — JEP 372 已移除 Nashorn。改用原生 DW(reduce、map 等)重写;如果逻辑确实需要 JVM 调用,则使用 java!,并根据模式 7 审查该路径。
  7. java! 前缀指向 sun.* / jdk.internal.* / 任何已封装的 JDK 内部 API — JEP 403 会严格阻止对 JDK 内部 API 的反射访问。改用 DataWeave 原生表示法重写(区域设置使用 {language, country} 映射,时区使用规范 ID 字符串)。
  8. 三字母时区标识符("PST"、"CST"、"EST"、"PST8PDT")— tzdb 变更及映射含义不明确。替换为规范的 IANA ID("America/Los_Angeles"、"America/Chicago"、"America/New_York")。

模型已从公开的 Java 17 迁移指南中了解每种模式的修复方法,因此无需附带扫描脚本。使用 Read 工具读取每个 DW 源文件,逐项检查清单,并在步骤 12 的计划中将每个命中项记录为 file:line — pattern-N — proposed fix。

连接器专属的强制转换检查(对连接器操作的 payload 使用 as Number、对连接器属性使用 now() as String、将 error.errorType.identifier 与 Mode-B 错误目录进行比较)也在同一轮读取中完成,无需单独扫描。

此步骤仅用于发现问题——grep/read DW 源码,标记所有不匹配项,但此处不要进行任何编辑(第 1 阶段不会写入项目文件)。发现结果将纳入计划的“DataWeave 下游影响”部分,并在步骤 12 编写;实际的 DW 重写将在步骤 15 进行。

  • 找出流程中的 DataWeave 脚本
  • 检查 Java 版本兼容性问题
  • 标记已弃用的函数或语法

步骤 10:检查 MUnit 兼容性

此步骤没有脚本。 Agent 会在计划综合阶段(步骤 12)使用 Read 工具直接读取每个 src/test/munit/**/*.xml。对于计划要重写的每个操作,标记:

  • <munit-tools:mock-when processor="<old-op>"> → 重命名计划条目
  • <munit-tools:then-return> 负载形状 → 标记架构不匹配
  • 读取操作响应字段的 <munit-tools:assert-that> → 标记交叉引用 DW
  • MUnit 错误路径中的 <on-error-propagate type="..."> → 应用计划中的错误类型映射

此步骤仅用于发现——搜索/读取测试并标记所有不匹配,但此处不要进行任何编辑。发现结果会纳入步骤 12 编写的计划「MUnit 下游影响」部分。实际测试编辑在步骤 15 进行,并由步骤 17(mvn test)验证。

  • 识别 MUnit 测试文件
  • 检查会影响测试的连接器操作变更
  • 标记需要更新的测试配置
  • 识别所需的模拟/断言变更

步骤 11:获取插件版本

确定升级后的应用应采用的目标 Mule Maven Plugin(MMP) 和 MUnit 版本。二者遵循相同的两部分模式:版本通过实时查询确定(确定性查询,绝不硬编码);该版本的兼容性范围则从发行说明中读取。

11a — 确定最新版本(实时、确定性)

这两个插件都会将每个发行版本发布到 Maven 元数据中,因此最新版本可通过确定性查询获得——取 <release> 元素(权威的「最新已发布版本」;比 <latest> 更可靠,后者可能指向快照版/RC 版):

  • MMP — https://repository.mulesoft.org/nexus/content/repositories/releases/org/mule/tools/maven/mule-maven-plugin/maven-metadata.xml
  • MUnit — https://repository.mulesoft.org/nexus/content/repositories/releases/com/mulesoft/munit/tools/munit-maven-plugin/maven-metadata.xml
curl -s "<metadata-url>" | grep -oE '<release>[^<]+</release>' | sed -E 's/<\/?release>//g'

建议项目使用最新的 MMP/MUnit。

将确定的两个版本都写回 tmp/upgrade-targets.json。 步骤 14 的 apply_runtime_bump.mjs 会读取 .muleMavenPlugin.to 以更新 <mule.maven.plugin.version>,并读取 .munit.to 以更新每个 MUnit 版本位置(<munit.version> 属性、munit-maven-plugin 插件块,以及 munit-runner / munit-tools 依赖项)——在此处设置它们,以便更新时使用实时查询得到的最新版本,绝不使用硬编码版本。如果存在,请复用步骤 3c 已确定的 MMP 值(tmp/latest-mmp.txt)——步骤 3c 仅针对 3.x 应用执行查询,因此如果缓存不存在(即应用原本已使用 4.x MMP),则重新获取:

MMP=$(cat tmp/latest-mmp.txt 2>/dev/null)
[ -z "$MMP" ] && MMP=$(curl -s "https://repository.mulesoft.org/nexus/content/repositories/releases/org/mule/tools/maven/mule-maven-plugin/maven-metadata.xml" | grep -oE '<release>[^<]+</release>' | sed -E 's/<\/?release>//g')
MUNIT=$(curl -s "https://repository.mulesoft.org/nexus/content/repositories/releases/com/mulesoft/munit/tools/munit-maven-plugin/maven-metadata.xml" | grep -oE '<release>[^<]+</release>' | sed -E 's/<\/?release>//g')
tmp=$(mktemp); jq --arg m "$MMP" --arg u "$MUNIT" '.muleMavenPlugin = {to: $m} | .munit = {to: $u}' tmp/upgrade-targets.json > "$tmp" && mv "$tmp" tmp/upgrade-targets.json
11b — 确定该版本的 Java / Maven / Mule 兼容性

由于目前没有 API 可用,Agent 会从发行说明页面读取兼容性信息。这些页面稳定、便于人工浏览,且会相互链接——不要构造特定版本的 URL;索引页会将每个版本列为超链接,请点击页面提供的链接:

  • MUnit 索引:https://docs.mulesoft.com/release-notes/munit/munit-release-notes — 列出每个 MUnit <x.y.z> Release Notes 链接(并标记已弃用的版本)。请点击 11a 中确定的版本对应的链接;跳过任何标记为 "Deprecated." 的条目。
  • 在该版本的页面中读取:支持的 OpenJDK 版本、支持的 Maven 范围、支持的 Mule runtime 范围。

格式合理性检查(提取内容来自散文,需要验证)。 读取后,先确认提取的值格式正确,再予以采信:Java 列表必须是已知集合(8, 11, 17, 21, …)的非空子集;Maven 值必须是有效范围;Mule 值必须是有效的版本约束。如果读取结果为空或格式错误,不要猜测或继续操作——停止并请用户确认。

参考值(已根据 MUnit 3.7.3 / MMP 4.10.1 发行说明核实,2026-08 — 请针对确定的版本重新核实):

  • MUnit 3.7.3 → OpenJDK 8, 11, 17, 21 · Maven 3.9.0–3.9.15 · Mule runtime ≥ 4.3
  • MMP 4.10.1 → OpenJDK 8, 11, 17, 21 · Maven 3.9.0–3.9.15

确认目标 Java(来自步骤 2b)和目标 Mule(来自步骤 6)均在确定的兼容范围内。记录确定的版本及兼容性,供步骤 12(计划)和 Maven 前置条件范围检查使用。


步骤 11.5:验证元数据覆盖范围(门禁)

在处理步骤 12 之前运行覆盖范围门禁。它会将 tmp/connector-usage/*.json 中的每个操作 / 源 / provider 与磁盘上的 Mode-A / Mode-B / Mode-C JSON 进行交叉核对;如果缺少任何必需的 describe,就会拒绝继续制定计划。

node <skill-dir>/scripts/verify_metadata_coverage.mjs

行为:

  • FAIL(退出码 1)——磁盘上缺少某个操作/provider 所需的 Mode-B 或 Mode-C JSON,而该操作/provider 存在于 Mode-A 中。请对这些(操作,provider)组合重新运行 describe_connector.mjs,然后重新运行此门禁。
  • WARN——某个操作/provider 出现在 usage_sites 中,但不在 Mode-A .operations[] / .configs[].connectionProviders[] 中。通常是重命名或移除;步骤 7 应已将候选项写入 <nick>-op-renames.json。默认情况下不会导致失败——传入 --strict 可令包含 WARN 行时也失败。
  • INFO——连接器的状态为 not_in_use,或者某个正在使用的配置在 Mode-A 中声明的 provider 数量为零(D7 回退)。对于 provider 为空的情况,Phase C 会直接使用 Mode-A .configs[];不需要 Mode-C。

此门禁以退出码 0 结束之前,不要继续到步骤 12。基于缺少逐操作 describe 的盲目计划,可能会悄然给出“无需重写”的结论,而缺失的 JSON 本可以揭示相反情况。


步骤 12:提交计划并征求批准

这是撰写计划的步骤,也是严格的审批门禁。不要调用 CLI,也不要运行脚本——LLM 会读取磁盘上已有的元数据和 usage JSON,并将明确、机械化且按符号列出的变更清单写入 tmp/upgrade-plan.md。对项目执行的所有操作都必须出现在此文件中,以便用户在变更触及工作树之前逐项批准或拒绝。计划必须准确描述哪些 file:line 要改成什么——绝不能只写意图(“更新 s3 操作”)。

此步骤的具体流程:

12.0 对照 Maven 交叉检查发现结果(软门禁)

编写计划之前,先询问 Maven 每个连接器实际解析到的版本,确认静态 POM 遍历(步骤 5)发现的是正确的当前版本。该遍历刻意限制在本地——它不会跟踪 <scope>import</scope> BOM 或传递性引入,因此,实际版本位于导入的 BOM 中的连接器可能会被悄然标记错误(值正确,归属错误),此时对发现位置进行步骤 14 的编辑就会无效。mvn dependency:tree 是确认解析值的唯一权威;本检查会将其与 connectors.json 进行比对。

# Reads tmp/connectors.json; runs `mvn dependency:tree` scoped to the connector GAVs.
# Piggybacks on the warm Maven state from the Step 3 baseline build.
node <skill-dir>/scripts/verify_dependency_tree.mjs . --against=existing

退出代码:0 表示所有连接器解析出的版本均与发现的版本一致(继续);1 表示操作错误(缺少 Maven/依赖树构建失败/无法读取 connectors.json)——解决问题,这是必须实际停止的情况;2 表示存在一个或多个不匹配项。此门禁为软门禁——不匹配不会中止工作流,因为尚未写入任何内容。请改为:

  • 阅读 tmp/dep-tree-verify-existing.json。对于 mismatches[](我们的值 ≠ Maven 的值)和 resolvedOnlyByTree[](我们无法在本地解析,但 Maven 可以——来源位于可编辑的本地链之外)中的每个条目,记录连接器、我们发现的值/归属、Maven 解析出的值以及 likelyCause(导入的 BOM/传递性引入/过时的 ~/.m2)。
  • 将每项作为明确条目纳入计划的已知风险/需操作者关注的事项部分,并在受影响连接器的 归属单元格中添加 — Maven 从本地链之外解析(参见风险)。这样,操作者在审批门禁处就会知道,对发现位置进行步骤 14/18 的编辑可能不会生效。
  • 如果存在不匹配项,不得静默自动继续:操作者应在步骤 12.4 门禁处决定该差异是否可以接受(例如连接器有意由 BOM 管理),或是否构成阻碍。

missingFromTree[] 中的条目(已声明,但从已解析依赖树中剔除——例如由运行时提供)仅供参考;只有当它们涉及计划要升级的连接器时才需记录。

12.1 完整性检查清单(编写计划前运行)

每个连接器都必须在磁盘上具备以下全部四种产物,并已针对实际使用情况完成交叉检查,之后才能提交计划:

  • tmp/connector-choices/<nick>-new.json — 已拟定的 GAV
  • tmp/connector-metadata/<nick>-new.json — Mode-A 摘要
  • tmp/connector-metadata/<nick>-new-<op>.json — 对于 usage.operations_used[] 与 new.operations[] 的交集中的每个 op,都有一份 Mode-B 按 op 生成的文件
  • tmp/connector-metadata/<nick>-new-<config>-<provider>.json — 对于工作流使用的每个(config,provider)组合,都有一份 Mode-C 文件
  • 第 3.5 步的卫生扫描标记的每个文件,都已列入计划的 §Flow-XML 卫生部分(或扫描无异常,且该部分明确说明了这一点)

第 11.5 步的 verify_metadata_coverage.mjs 门禁已运行过此存在性检查的机械化版本;如果其退出代码为 0,则所有产物均已就绪。如果缺少任何产物,请返回步骤 5/6/7,不要提交不完整的计划。步骤 7 中必须完成的差异检查(属性重命名、错误类型、provider 元素集合成员关系、递归子树)也必须已经完成——此清单用于确认这些检查的结果都已落实为下方计划中的条目。如果任何差异检查发现的符号未被计划列出,则该计划不完整——返回步骤 7,重新描述并重新综合。“技能声称成功后构建失败”几乎总是因为元数据虽已存在,却被忽略了。

12.2 根据已有数据解析重命名

不要推迟到第 16 步。 第 11.5 步输出的每一行 WARN 都表示重命名信号(WARN <nick>/<op-or-provider> — not in Mode-A ...)。对于每一行 WARN:

  • 操作重命名:交叉查阅 tmp/connector-metadata/<nick>-new.json .operations[](所有新操作名称)。选择语义上最接近旧操作名称的匹配项,并通过读取其 Mode-B 文件 tmp/connector-metadata/<nick>-new-<newOp>.json .attributes[] 来确认:新操作是否接受流 XML 在旧元素上设置的属性?如果是,将重写内容编码为计划条目:Rewrite <ns>:<oldOp> → <ns>:<newOp>,并在行内列出属性差异(重命名、移除、新增必需属性)。
  • 提供程序重命名:交叉查阅 Mode-A .configs[].connectionProviders[](该配置的所有新提供程序名称),并读取每个候选项的 Mode-C 描述文件 tmp/connector-metadata/<nick>-new-<config>-<newProvider>.json。选择属性最能覆盖流 XML 在旧提供程序元素上设置内容的新提供程序(搜索流 XML:grep -c 'ns:oldProvider' src/main/mule/*.xml,然后列出其属性)。输出 Rewrite <ns>:<oldProvider> → <ns>:<newProvider>,并注明属性差异。
  • LLM 已具备所需的所有输入——旧名称(来自 tmp/connector-usage/<nick>.json .config_providers_used[] / .operations_used[])、新名称(来自 Mode-A .operations[] / Mode-A .configs[].connectionProviders[]),以及每个目标的属性结构(来自 Mode-B / Mode-C JSON)。无需新脚本,也无需新的 AskUserQuestion——只需在展示计划前,将重命名条目汇总到 tmp/upgrade-plan.md 中。只有在匹配确实存在歧义(2 个或更多新候选项的属性覆盖程度相同)时,才通过 AskUserQuestion 暂停。
  • 新增必需属性——除重命名外,还要将每个已用操作 / 提供程序 / 配置的 Mode-B/Mode-C .attributes[] 中 required: true 的属性,与流 XML 对应元素实际设置的属性进行差异比较。对于当前流 XML 中未设置的每个新增必需属性:
    • 如果设置了 .default → 输出 Add <ns>:<element> @<attr>="<default>"(示例:crypto 2.x 的 <crypto:jce-config> 现在要求 type,且 .default = "JCEKS" → 计划条目 Add crypto:jce-config @type="JCEKS")。
    • 否则,如果 .type == "enum" → 选择 .values[0],并在条目中注明 (picked first enum value; verify)。
    • 否则 → 在最终确定计划前,将其作为 AskUserQuestion 条目提出(Connector <nick> op/provider <name> requires new attribute <attr> (<type>) — please supply a value)。
  • 这会在计划阶段利用已获取的 Mode-B/C 数据,捕获“XSD 指定属性 X 为必需属性,但你没有设置”这类问题,而不是让第 16 步浪费重试次数,从 mvn 错误文本中反向推断枚举值。
12.3 编写 tmp/upgrade-plan.md

将 12.2 中的重命名要点合并进去,编写 tmp/upgrade-plan.md。规划输入(均已在磁盘上):tmp/upgrade-targets.json(mule、java、connectors 的 from/to),tmp/connector-metadata/<nick>-new.json(Mode-A),…-new-<op>.json(Mode-B,含 attributes[]、childElements[]、errorTypes[]、.output*),…-new-<config>-<provider>.json(Mode-C),tmp/connector-usage/<nick>.json(使用位置、attributes_set、捕获/抛出的 errorTypes、namespace_prefix_changed)。每个部分都必须注明其依据的具体 JSON 文件——审阅者会通过交叉核对引用来验证计划。

# Upgrade Plan — <project-name>

## Targets
- Mule runtime: <from> → <to>
- Java: <from> → <to>

### Connectors

**Connectors (<N>)** — recommended to move to the **latest version supporting Java <targetJava> + Mule <targetMule>**.

| Connector | Current version and Java compatibility | Updated version | Owned by | Notes |
|---|---|---|---|---|
| <name> | <currentVersion> — Java <supportedJava…> | <targetVersion> | <owner> | <note> |

Render this as a single table — one row per connector. **Sort rows so any with a non-`—` Note float to the top.** Column sources (all already on disk — cite, never invent):

- **Connector** — the human-readable connector name (not the internal `<nick>`).
- **Current version and Java compatibility** — `<currentVersion>` + its Java window from `connector-java-compat.json .connectors[].supportedJava` (Step 5b). Keep the version and its Java window in this one column — the Java window is an attribute of that version. Bold `Java 8 only` when `supportedJava == [8]` (no fallback). When `currentVersionDelisted: true`, `supportedJava` is empty — show `<currentVersion> — delisted from Exchange` instead of a blank Java window.
- **Updated version** — `targetVersion` from `target-connectors.json` (Step 6): the latest version supporting **both** the target Java and target Mule. When `changed: false` (current already IS the latest compatible), show the same version and set the Note to `already latest compatible — no change`.
- **Owned by** — provenance from `connectors.json` (Step 5). Key off `resolvedFrom`: `child` → **child**; `parent` → **parent**; `ancestor` → **grandparent+**. When `versionManagedIn` is present (the `<dependencyManagement>` case), append its basename so the operator sees the exact owning POM — e.g. `parent (parent/pom.xml)`; for a version inherited inline from an ancestor's `<dependencies>`, `versionManagedIn` is null, so just show `parent` / `grandparent`. A non-`child` owner is where the bump will actually be **written**: Step 14's `apply_parent_pom_fork.mjs --phase=edit` writes the new versions into that ancestor in place, and Step 18's `--phase=fork` bumps its `<version>` and repoints the downstream `<parent>` — rather than editing the child. Surfacing it here lets the operator see, at the approval gate, exactly which shared POMs the upgrade will touch and fork.
- **Notes** — derived deterministically from data already on disk; **never** free-text impression. Empty → `—`. Compose from:
  - MAJOR bump — `major(targetVersion) != major(currentVersion)` (semver on `target-connectors.json`).
  - prefix `<from>` → `<to>` — `usage.namespace_prefix_changed` (Step 7b).
  - operation / config / error-type change — the connector has ≥1 rewrite bullet in the §Operations / §Configs / §error-type sections below (Step 7 diffs).
  - `current is Java 8 only` — `supportedJava == [8]`.
  - `custom — verify manually` — connector was unverifiable on Exchange (custom/private).
  - `current version delisted from Exchange` — `currentVersionDelisted: true` (current Java compat unverified; target resolved from the newest available version).
  - `already latest compatible — no change` — `changed: false`.

**Do NOT repeat the target-Java window per row** (it is constant — every Updated version supports the target by construction; the header states this once). **A `—` Note is only trustworthy after the Step 11.5 coverage gate passed** — it means Step 7's diffs came back empty, not that analysis was skipped.

## Namespace prefix changes
- <nick>: <old-prefix> → <new-prefix>   (source: usage.namespace_prefix_changed)

## Operations
For each op the flow uses:

### <op-name> (op_OLD → op_NEW)
- Kind: straight-match | rename | true-removal
- Sources: usage.operations_used[], per-op JSON (<nick>-new-<op>.json)
- Sites (from usage.usage_sites[]):
  - <file>:<line>   attributes_set: [a="…", b="…"]
- Per-site edit contract:
  - Element rename: <old-prefix>:<old-op> → <new-prefix>:<new-op>
  - Attribute renames:
    - `bucketName` → `bucket`
    - `content`    → PROMOTE TO CHILD ELEMENT `<s3:content>#[payload]</s3:content>`
      (source: <nick>-new-<op>.json .childElements[])
  - Removed attributes:
    - `useVersioning` — dropped
  - New required attributes/children:
    - <name> (required=true, defaultValue=<x>) — insert with default
- Error-type mapping (per-op):
  - S3:BUCKET_NOT_FOUND → S3:NO_SUCH_BUCKET   (source: <nick>-new-<op>.json .errorTypes[])

## Configs / connection providers
For each (config, provider) pair the flow uses:

### <config-nick> (<config-name>, provider <provider-name>)
- Sources: <nick>-new-<config>-<provider>.json
- Config element:  <old-prefix>:<old-config-element> → <new-prefix>:<.elementName>
- Connection element:  <old-prefix>:<old-connection-element> → <new-prefix>:<.connectionProviders[…].elementName>
- Attribute renames (on the connection element)
- Removed connection attributes / Added required connection attributes-children
- Sites (from usage.usage_sites[])

## Connector-wide error type renames
Enumerated from <nick>-new.json .errorTypes[] (OLD types from usage.errorTypes_caught[] / errorTypes_raised[]):
- <OLD_TYPE> → <NEW_TYPE>
  - **Catch sites (where the edit actually happens):** grep the flow XML for the OLD type — `grep -rn 'type="<OLD_TYPE>"' src/main/mule/*.xml` — and list **every** `<on-error-propagate type="…">` / `<on-error-continue type="…">` `<file>:<line>` that names it. These handler sites are what Step 15 edits, NOT the operation that *raises* the error. Citing only the raise-site (the `<prefix:op>` element) leaves the `on-error-*` handler untouched and the build fails at `process-classes` with `Could not find error '<OLD_TYPE>'`. Enumerate one bullet per catch site.

## DataWeave downstream impact
For every DW consumer that reads output from an op the plan will rewrite:
- Symbol list read from the op's response
- Diff against Mode-B .output* keys:
  - Present in Mode-B: no change
  - Absent, sibling present (probable rename): proposed rewrite (with source citation)
  - Absent AND Mode-B has NO .output* keys: SITE FLAGGED FOR OPERATOR

## Custom Java downstream impact
Findings from Step 8a (read from `src/main/java/**`, `src/test/java/**`, `<java:invoke>`/`<java:new>`/`<java:invoke-static>` targets, and `java!` callouts). One bullet per site the upgrade must edit for the app to compile/run on the target Java, in the format `file:line · problematic line · category · proposed fix`:
- `<file>:<line>` · `<verbatim problematic line>` · <category: removed package / restricted internal / deprecated method / locale> · <proposed fix> (e.g. `src/main/java/com/acme/XmlBinder.java:12 · import javax.xml.bind.*; · removed package (JEP 320, gone in Java 11) · add jakarta.xml.bind:jakarta.xml.bind-api:4.0.x + jaxb-runtime, or migrate to jakarta.*`)
- For any fix that adds a dependency, state the exact coordinate here — the add is applied in Step 15.4.
- (write "none — no custom Java source impact" if Step 8a found nothing; do NOT write this without having read the files `find`/`grep` surfaced)

Step 8b runtime-only surfaces (Spring beans, Serializable state, custom security/TLS, custom log4j appenders, JDK-module reflection) are NOT listed here — they go under §Known risks / operator-attention items because the skill can't verify them and a green build doesn't prove them safe.

## Parent-POM forks (shared ancestor edits)
The ancestor counterpart to the child edits below — lists exactly which shared POMs the upgrade will bump (Step 14) then version-fork + repoint (Step 18). **Do NOT hand-derive this from `connectors.json` and do NOT recall owners from memory** — that is the class of error a free-authored list introduces (a connector reported under the wrong ancestor). Instead render it from the fork script's own owner→connector computation. Run its edit-phase dry-run once (writes nothing — it only reads Step 5's `connectors.json` and Step 6's `target-connectors.json`, both already on disk):

```bash
node <skill-dir>/scripts/apply_parent_pom_fork.mjs . --phase=edit --dry-run >/dev/null
jq -r '.ancestorsForked[] | "- `\(.pomPath)` (\(.artifactId)) owns: " + (.connectors | join(", "))' tmp/parent-pom-edit-dryrun.json

为每个 ancestorsForked[] 条目原样写一条项目符号——pomPath 是所属 POM,connectors[] 是它的 "<artifactId> -> <target>" 升级项。不要重新指定、合并或重新标注所属方;脚本已沿完整链路遍历 <dependencies> 和 <dependencyManagement>,确定每个 connector 实际声明其版本的 POM。

  • <pomPath>(<artifactId>)负责:<connectorA> -> <to>、<connectorB> -> <to>、…
  • (当 ancestorsForked[] 为空时,写“无——每个 connector 都由子级拥有;没有要分叉的祖先”)

如果有 warnings[],也要列出 dry-run 的警告(jq -r '.warnings[]' tmp/parent-pom-edit-dryrun.json)——例如某个由祖先声明的 connector 没有已解析的目标——以便操作员在审批环节看到。

为操作员说明这些项目的含义,不要引用内部步骤编号——计划的读者关心的是要做什么,而不是技能的步骤索引:“先原地升级 connector 版本;每个所属祖先的自身 <version> 都要分叉,并且只有在构建成功后才重新指向子级 <parent>。仅在本地编辑——不发布到 Exchange。”分叉范围意味着祖先还会升级它所声明、且应用恰好也在使用的任何其他 connector(例如子级也固定了同一版本的副本)——即使 nearest-wins 仍使子级的副本生效,这些项目也会显示在此处的祖先名下。现在运行 dry-run(而不是转录来源信息),可确保计划与编辑阶段将实际写入的内容完全一致。

pom.xml

这些内容始终写入子级(apply_runtime_bump.mjs),绝不会写入祖先级——即使该属性目前仅存在于父级或祖父级中(会插入子级覆盖项;Maven 的最近优先规则会使其生效,而无需修改共享 POM)。连接器表格的 Owned by 列适用于连接器;本节仅适用于子级。

  • <app.runtime>: → (若不存在则插入)
  • / java.version / jdk.version: → (仅在存在时逐项升级;绝不插入)
  • maven-compiler-plugin 构建级配置 → Java :仅当本次运行升级确实会修改构建级别时,才包含此条——也就是说,应用会编译 Java(src/main/java/src/test/java),或已声明构建级 maven.compiler.*(项目或配置文件的 <properties> 下的属性,或 maven-compiler-plugin 中的内联 <source>/<target>/<release>)。否则完全省略此条——没有构建级配置的纯 flow/DataWeave 应用不应出现编译器条目。在其他作用域中找到的 maven.compiler.*——例如 cloudHubDeployment 等插件/部署 <properties> 中——不属于构建级别,不会自动升级,也不算作此处的“已声明”;不要将其列为自动编辑项;它会通过下方的 Step-14 WARN/确认流程告知操作人员。显示此条时:级别会在其所在位置原位升级(无需手动编辑,差异最小,并保留在自身作用域中——例如 <profile> 内);已声明的属性或内联级别会在原处升级;仅有内联 <target>/<source> 时,会将其转换为 <release><to></release> 来补全(release 绝不会与 source/target 共存);如果应用包含自定义 Java 代码且没有在任何位置声明级别,则会插入 maven.compiler.release = 。
    • 绝不自动升级。运行升级脚本会在写入时(Step 14)针对每项发出 WARN,并附上确切的 tag=value;将每条 WARN 转达给操作人员,询问是否更新每一项,并且只应用已确认的项目,且须在报告的位置修改(不要猜测)。转达任何此类项目时,应如实说明具体标签、其所在位置(例如 cloudHubDeployment 内)以及它会或不会影响什么;绝不要将操作人员的 POM 配置描述为“非标准”或偏离惯例——这些配置可能只是超出脚本自动管理的范围。涵盖:测试编译级别(maven.compiler.testSource/testTarget/testRelease)或通过 <compilerArgs> / -source / --release 设置的级别;插件/部署 <properties>(例如 cloudHubDeployment)中的过时 maven.compiler.*;目标版本不匹配的 maven-toolchains-plugin <jdk><version>;其范围可能会拒绝目标 JDK 的 maven-enforcer-plugin <requireJavaVersion>。
  • <mule.maven.plugin.version>:升级到最新的 MMP(.muleMavenPlugin.to,在 Step 11a 中实时解析)

mule-artifact.json

  • minMuleVersion: → (截断为 x.y.0——例如 4.9.19 → 4.9.0;按 MINOR 版本线声明应用所需的功能,与 ACB/Studio 和 Introspection Service 保持一致。此操作造成的测试运行时风险由 munit 配置中的 <runtimeVersion>${app.runtime}</runtimeVersion> 固定项消除)
  • javaSpecificationVersions:确保包含 (若不存在则插入数组,适用于任何目标 Java 版本)

pom.xml — munit-maven-plugin

  • MUnit 版本:将 <munit.version> 属性 / munit-maven-plugin 插件 / munit-runner / munit-tools 升级到最新的 MUnit(.munit.to,在 Step 11a 中实时解析)
  • :若 munit 配置中不存在,则插入 ${app.runtime}(绝不覆盖已有固定项)——强制 MUnit 的内嵌测试运行时使用完整的目标补丁版本,而不是回退到 x.y.0 的 minMuleVersion 下限;否则会启动较旧的运行时,并导致带有 JAVA_25 注解的连接器出现 EnumConstantNotPresentException

xsi:schemaLocation URL

  • apply_connector_pin.mjs 将以确定性的方式重写 mule-.xsd URL

Flow-XML 卫生检查(来自 Step 3.5 扫描)

对于 Step 3.5 卫生检查标记的每个文件(存在潜在未绑定前缀,在 4.9+ 上会失败),各列出一个条目,包括其他章节未涉及的文件:

  • :向根 添加 xmlns:<prefix>="<namespace-uri>"(违规前缀:;来源:Step 3.5 xmllint stderr)
  • (如果扫描无问题,请写“none — all flow files namespace-well-formed”)

自定义/非连接器库(需要操作员确认)

从 connectors.json .customLibraries[](Step 5)枚举——应用的非连接器、非测试/provided、非平台依赖(例如,共享错误处理器 jar、工具库、直接依赖的第三方 jar)。此技能不会升级这些库——没有可解析的 Exchange 目标版本——但它们很可能是针对旧版 Java/运行时编译的,升级后可能会出问题(UnsupportedClassVersionError、NoSuchMethodError,或堆栈跟踪完全位于库自身包中的 ClassCastException,而且往往只在部署/运行时出现;绿色的 -DskipTests 构建无法发现这些问题)。每个库列出一个条目:

  • <groupId>:<artifactId> <version>(归属于 <resolvedFrom>)——请确认其与 Java <targetJava> + Mule <targetMule> 兼容,或提供更新后的坐标。不会自动升级。
  • (如果 customLibraries[] 为空,请写“none — no custom/non-connector libraries detected”)

已知风险/需要操作员关注的事项

  • Java 版本窗口警告、标记为需操作员处理的 DW 位置、没有重命名目标的真正移除操作等。
  • 自定义 Java 的运行时专属风险点(Step 8b)——通过类实例化的 Spring bean、跨 Object Store/VM/集群传递的 Serializable 状态、自定义安全/加密/TLS Java、自定义 log4j2 appender,以及 JDK 模块反射(setAccessible/--add-opens)。每个 file:line 列出一个条目,并说明它为何可能在目标 Java 上出问题。这些代码可通过编译,而且 -DskipTests 构建不会覆盖它们——操作员必须在 Step 12.4 关卡确认它们在 Java <targetJava> 上安全,因为构建成功并不能证明这一点。
  • 依赖树交叉检查差异(来自 Step 12.0 dep-tree-verify-existing.json):任何 Maven 解析出的版本与我们的发现不一致的连接器,或解析结果来自可编辑本地链之外(导入的 BOM/传递依赖/过时的 ~/.m2)的连接器——针对发现位置进行的 Step 14/18 编辑可能不会生效。每项差异列出一个条目,并注明我们的值、Maven 的值及可能原因。

Authoring rules:

- **Never invent an operation, attribute, or child element.** Every rename claim must have a corresponding entry in a Mode-B / Mode-C JSON on disk.
- **Preserve business intent in the plan.** `doc:name`, DataWeave payloads, `config-ref` values, error-handler shapes are not part of the upgrade; they must survive Execution unchanged. Note it explicitly in the plan when a site has DW / config-ref / doc:name so reviewers can spot an accidental drop.
- **Flag ambiguity.** If an OLD op has no plausible NEW rename target, mark the site as `true-removal — operator attention required`. Do NOT silently guess. If a DW site has no Mode-B `.output*` shape catalog, list every symbol read and flag the site for operator confirmation.

### 12.4 Present and gate

1. `Read` the file `tmp/upgrade-plan.md` (with the rename bullets folded in).
2. Print its full contents inline in the response as fenced markdown so the user can review without opening another file.
3. **Custom-library confirmation (only if `connectors.json .customLibraries[]` is non-empty).** Before the main approval gate, raise these proactively with a dedicated `AskUserQuestion` — the skill can neither verify nor upgrade a non-connector jar, so the operator must make the call while the target Java/Mule is in view. **State the coordinates only — do NOT assert or imply whether they support the target Java/Mule.** You have no compatibility source for these jars (not on Exchange, source not in the workspace, a green `-DskipTests` build will not exercise the library's own bytecode), so any "these support Java N" claim is an unverifiable guess and must not appear — even if you happen to believe it from background knowledge; the whole point of this gate is that the *operator* owns that judgment, not the skill. Phrase the question neutrally, e.g. *"Your app depends on these non-connector libraries the skill can't verify or auto-upgrade: `<list>`. Confirm they work on Java `<targetJava>` + Mule `<targetMule>`, or tell me what to change."* List every `customLibraries[]` coordinate and offer three options: `I've confirmed these are compatible — keep as-is`, `Some need a version change — I'll supply coordinates`, `Not sure — flag and continue at my own risk`. On "supply coordinates," collect them and fold the replacements into the plan's pom.xml section (the operator's explicit version is a normal version edit — not an auto-resolved bump). If `customLibraries[]` is empty, skip this sub-step entirely — do not prompt.
4. Use `AskUserQuestion` with three options:
   - `Yes, proceed to Execution`
   - `No, I want to change the plan`
   - `No, cancel the upgrade`
5. **WAIT for the explicit "Yes, proceed to Execution."** before advancing to Step 13.

On `No, change`: collect specifics via a follow-up `AskUserQuestion`, loop back to the affected step (5/6/7/9/10), re-synthesize the plan, re-present. Do NOT rerun Step 1.

On `No, cancel`: stop the workflow. Leave `tmp/` in place for inspection.

- Display all version updates
- Show connector version changes
- Show operation/config/error type changes
- Show plugin updates
- Flag blockers
- **⚠️ APPROVAL GATE:** Wait for user approval before proceeding to execution

---

# Phase 2: Execute

## Step 13: Download Target Runtime and Java

Ensure the **finalized target** Mule Runtime and target JDK are present on disk before the build. Both are driven by the approved target in `tmp/upgrade-targets.json` (`.mule.to` = full Mule version, `.java.to` = Java major) — never a hardcoded pair. Each resolver checks the local install dir first and only downloads when the version is absent. **A version can be downloaded only if `anypoint-cli-v4 dx mule runtime list` returns it** — that list is the sole source of truth for what is downloadable, so the target must always be one it offers (Step 4 already resolves the target against this same list).

```bash
# Target JDK — reads the target Java MAJOR from tmp/upgrade-targets.json.
node <skill-dir>/scripts/resolve_jdk.mjs "$(jq -r '.java.to' tmp/upgrade-targets.json)" .

# Target Mule Runtime — reads the full target Mule version from tmp/upgrade-targets.json.
node <skill-dir>/scripts/resolve_runtime.mjs "$(jq -r '.mule.to' tmp/upgrade-targets.json)" .
  • resolve_jdk.mjs <major> 会复用 ~/AnypointCodeBuilder/java 下该主版本的已安装 JDK;否则会下载该主版本运行时列表指定的构建版本(dx mule jdk download)。写入 tmp/resolve-jdk-<major>.json(javaHome)。失败时退出码为 1。
  • resolve_runtime.mjs <version> 会复用 ~/AnypointCodeBuilder/runtime 下已安装的 mule-enterprise-standalone-<version>;否则会下载该版本(dx mule runtime download),但仅限运行时列表提供该版本时。写入 tmp/resolve-runtime-<version>.json(runtimePath)。失败时退出码为 1。

两个脚本都不会设置 JAVA_HOME 或任何运行时路径——它们只会确保发行版已就绪并报告其位置。如果任一脚本以非零状态退出,请通过 AskUserQuestion 将其 stdout 呈现给用户,然后等待(下载需要正常工作的 CLI 身份验证/网络)。此处解析的目标 JDK(tmp/resolve-jdk-<target-java>.json)会在 Step 16 的构建中通过内联的 JAVA_HOME=$(jq -r .javaHome ...) mvn 前缀使用。

范围:此步骤仅配置应用的目标运行时/JDK。


Step 14:更新文件——仅版本

确定性地重写版本——每次脚本调用单独放在一个 Bash 响应中。顺序很重要:先提升草稿 → 再升级运行时 → 最后逐个固定连接器版本。

node <skill-dir>/scripts/promote_new_connector_pins.mjs
node <skill-dir>/scripts/apply_runtime_bump.mjs .

apply_runtime_bump.mjs 只会改写 pom.xml / mule-artifact.json 中的版本,不会运行 Java。强制使用正确的 JDK,是通过在命令中将构建固定到已解析的目标 JDK来实现的:Step 16 中的 mvn 调用形式为 JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<target-java>.json) mvn clean package …,与基线构建(Step 3c)完全相同。不会读取用户当前环境中的 JAVA_HOME / PATH;使用的是 Step 13 中 resolve_jdk.mjs 找到或安装的 JDK(位于 ACB java 目录下,若不存在则下载)。

然后逐个处理连接器:

for nick in $(jq -r '.connectors[].nick' tmp/upgrade-targets.json); do
  node <skill-dir>/scripts/apply_connector_pin.mjs "$nick" .
done

读取循环输出——deferred-to-parent 并非失败。 apply_connector_pin.mjs 只会编辑子级 pom.xml。对于版本位于某个祖先 POM 中的连接器(继承的依赖,或由祖先的 <dependencyManagement> 管理的无版本子级依赖),子级中没有可编辑的内容——脚本会查询 Step 5 的来源记录(resolvedFrom / versionManagedIn),并将该固定项报告为 "status": "deferred-to-parent"(ownedBy 指明所属的祖先 POM,例如 parent/pom.xml),而不是 error。这些升级由下文的 apply_parent_pom_fork.mjs --phase=edit 负责。此处的 "status": "error" 表示真正的子级所有写入失败——只有这种固定状态应该导致 Step 14 停止。

此处不要重新执行 describe。 Plan Phase 已经在 tmp/connector-metadata/<nick>-new.json 中内省了每个固定连接器(Step 7),所有下游读取程序都会使用该文件——apply_connector_pin.mjs(命名空间)、verify_metadata_coverage.mjs(覆盖率检查),以及 Step 16 的第 1–5 类。再次对已固定的连接器执行 describe(写入不带后缀的 <nick>.json),只有 Step 16 的第 6 类恢复流程(xsi:schemaLocation 404)会读取该文件,而这种情况几乎不会发生——因此,在此处无条件地对全部 N 个连接器重新执行 describe,会额外消耗 N 次 Java 17 CLI 调用,只为填充绿色构建根本不会打开的文件。因此,该操作会延迟执行:仅当需要已固定命名空间的真实信息来解决架构 URL 故障时,Step 16 第 6 类才会按需重新 describe 单个故障连接器。参见 Step 16 · 16.1 · 第 6 类。

然后,如果有任何连接器归本地祖先 POM(父级/祖父级)所有,就在原处编辑其所有版本。 上述两个子级写入程序只会编辑子级 pom.xml;父级 <dependencies> / <dependencyManagement> 中声明的连接器不由它们处理——这些由 apply_parent_pom_fork.mjs --phase=edit 负责。该脚本读取来源记录(Step 5 中的 resolvedFrom / versionManagedIn)和已解析的目标版本(Step 6),并将升级写入实际拥有该版本的 POM。不会分叉运行时/插件属性——app.runtime / mule.maven.plugin.version / Java 属性属于应用级配置,上文的 apply_runtime_bump.mjs 已将其写入子级(如果该属性只存在于祖先中,则采用仅在不存在时插入的方式;Maven 的就近优先规则会使子级值生效,而无需修改共享祖先)。运行一次即可(它会处理整个链):

node <skill-dir>/scripts/apply_parent_pom_fork.mjs . --phase=edit

脚本会在首次写入之前,将即将编辑的每个祖先快照保存到 tmp/pom-backups/,确保快照未经修改(仅当文件不存在时才创建,因此重新运行编辑阶段会保留原始内容)——无需单独执行备份步骤。该快照有两个用途:Step 16 放弃构建时,可以恢复未经修改的祖先(编辑失败时绝不能让共享祖先处于修改状态);Step 18 会根据快照计算分叉 <version>,使 --fork-bump 重复运行时保持幂等。由于脚本只会备份它实际修改的 POM,因此快照集合绝不会与编辑集合不一致。 输出一段简短摘要(包括 backed up N ancestor POM(s) pristine → tmp/pom-backups/);完整结果会写入 tmp/parent-pom-edit.json(其中包含快照路径的 backedUp[] 数组)。第 18 步的条件取决于 ancestorsForked[] 是否非空,因此请读取该文件(jq '.ancestorsForked' tmp/parent-pom-edit.json),以获取持久记录。

apply_parent_pom_fork.mjs --phase=edit 会原地提升每个所属祖先管理的连接器版本——它不会修改祖先自身的 <version> 或子项目的 <parent> 引用。这是有意为之:保持祖先的 <version> 不变,意味着子项目现有的 <parent><version> 仍然匹配,因此第 16/17 步的本地构建会通过 <relativePath> 直接从磁盘上的祖先解析新版本。<version> 提升和子项目重指向(即“分叉”)会在绿色构建后才于第 18 步进行。该步骤按链深度从深到浅处理,应用覆盖整个分叉的连接器范围(提升所属祖先中声明的所有连接器;对没有目标版本的连接器发出警告,但绝不会因此硬性停止),并在退出前验证每次提升后都能通过未变更的父级链接从子项目重新解析。它只会修改连接器——app.runtime / mule.maven.plugin.version / Java 属性绝不会被分叉(它们已由 apply_runtime_bump.mjs 写入子项目)。退出代码非零表示写入未生效——请停止并检查其 verify.checks[],不要继续构建。当所有连接器都归子项目所有时,它会输出一条“无事可做”提示并以代码 0 退出。**原地编辑、构建,然后分叉:**原地编辑会在构建前执行,以便第 16 步测试的正是最终发布的版本;如果构建循环最终失败,请从 tmp/pom-backups/ 恢复祖先(通过 git 还原子项目),以免共享祖先保持被修改的状态。

上述固定顺序(提升草稿版本 → 运行时版本提升 → 逐个连接器固定版本 → 原地编辑父级 POM)就是构建前准备——第 16 步运行 mvn 前无需单独进行验证。apply_connector_pin.mjs 会确定性地重写 xsi:schemaLocation(每个连接器昵称调用一次);切勿手动编辑这些 URL。apply_runtime_bump.mjs 会从 tmp/upgrade-targets.json 读取 .mule.to / .java.to,并提升 pom.xml 及 mule-artifact.json 中的版本:

  • <app.runtime> — 始终确保存在:如果已存在则升级,否则在 <properties> 中插入(从未声明过它的应用也会固定到目标运行时版本)。
  • Java 版本属性 — 仅当 <javaVersion>、java.version、jdk.version 已存在时才升级(绝不插入——POM 未声明某个标签时,不应凭空添加)。
  • 编译器级别(normalizeCompilerLevel) — 将 maven-compiler-plugin 的级别原地升级为目标 Java 版本,无论它位于何处,从而最大限度缩小差异,且级别不会脱离其作用域。此操作保证以下不变量:release 不会与 source/target 并存(maven-compiler-plugin 会拒绝这种配置:"Cannot have both release and target/source"),且单独声明的 target/source 会补全,而不会留下一半配置。具体情况如下:
    • 级别仅在插件配置中 — 在其所在区块中升级(包括 <execution>、<pluginManagement> 和 <profile> 中的编译器区块,因此配置在 profile 作用域中的级别仍留在该作用域内):升级 <release>;同时升级 <source> 和 <target>;将单独的 <target> 或 <source> 转换为 <release><target-java></release>(单独的 target 会让 source 使用插件默认值,而较新的 JDK 会拒绝这种配置——因此会自动修复裸写的 <target>1.8</target>,无需手动编辑)。
    • 级别仅在属性中 — 升级 maven.compiler.release(删除残留的 source/target 属性);否则将 source/target 属性都设为目标值(补齐缺失的一项)。
    • 插件配置和属性中都存在级别(少见,容易冲突)— 合并到属性所在位置(移除内联配置),以避免出现 release 与 source/target 并列冲突。
    • 未声明任何内容 + 存在自定义 Java 代码(src/main/java/src/test/java)— 插入 maven.compiler.release = 目标 Java 版本(若 POM 没有 <properties> 区块,则创建该区块),但会先检查:如果级别已通过 <compilerArgs>(-source/--release)设置,则不会插入(否则会与 javac 冲突),而是发出 WARN。纯 Flow/DataWeave/MUnit-XML 应用若未声明级别,则不会添加任何内容。
    • 不会触碰 mule-maven-plugin 中用于部署的 <target>(仅处理 maven-compiler-plugin 区块)。**不会自动升级——run-bump 会在写入时(第 14 步)发出 WARN,并附上确切值。请将每条 WARN 都转告操作人员,询问是否要更新,并且只应用已确认的项目,且须在报告的位置修改。应用任何已确认的编辑前,先重新 Read pom.xml——apply_runtime_bump.mjs 已重写该文件,因此编辑时要基于当前文件。会发出 WARN 的情况:
      • test-compile 级别(maven.compiler.testSource/testTarget/testRelease、内联的 <testSource>/<testTarget>/<testRelease>),以及通过 <compilerArgs>/<compilerArgument>(-source/--release)设置的级别。
      • maven.compiler.{source,target,release} 在插件/部署 <properties> 中仍为旧值(例如 mule-maven-plugin > cloudHubDeployment > <properties>)——这些是部署键/值,而不是构建级别,因此绝不会被重写;WARN 会报告确切的 tag=value,供操作人员选择是否更新。
      • maven-toolchains-plugin 固定的 <jdk><version> 不是目标 Java 版本——无论 JAVA_HOME 设置为何,工具链都会选择指定的 JDK,因此旧固定值可能导致静默地使用旧 JDK 编译(请核实/更新固定值)。
      • maven-enforcer-plugin <requireJavaVersion>——其版本范围可能拒绝目标 JDK 并导致构建失败;请核实范围是否允许目标 Java 版本。
  • <mule.maven.plugin.version> — 如果已存在则升级,否则插入为 .muleMavenPlugin.to(第 11a 步实时解析得到的最新 MMP)。如果 tmp/upgrade-targets.json 中没有该字段,则保持属性不变。
  • MUnit 版本 — 将以下三个位置都升级为 .munit.to(第 11a 步实时解析得到的最新 MUnit):<munit.version> 属性(若已声明)、munit-maven-plugin 插件区块,以及 munit-runner / munit-tools 依赖区块。由 ${property} 驱动的版本会随属性升级;硬编码的字面值则会原地重写。不要手动编辑 MUnit 版本——此脚本负责管理它们,与连接器固定版本和 MMP 一样。如果 .munit.to 不存在,则保持这些位置不变。
  • munit-maven-plugin <runtimeVersion> — 在插件的 <configuration> 中插入 ${app.runtime}(如果插件没有 <configuration> 区块,则创建一个)。仅在不存在时插入:现有的 <runtimeVersion>(团队有意设置的固定值)绝不会被覆盖。这样可将 MUnit 内嵌测试运行时固定到完整的目标补丁版本。若不这样做,MUnit 会回退到 minMuleVersion——我们会正确地将其写为 x.y.0 功能版本线——并启动较旧的 x.y.0 运行时;该版本的 mule-sdk-api JavaVersion 枚举可能缺少较新的常量(例如 JAVA_25),因此使用目标补丁版本的 @SupportedJavaVersions 编译的连接器会在部署时因 EnumConstantNotPresentException 而无法解析扩展模型,MUnit 也就无法运行。
  • mule-artifact.json — 将 minMuleVersion 设为 x.y.0 功能版本线(例如,目标为 4.9.19 → minMuleVersion 4.9.0)。这是符合平台规范的形式:minMuleVersion 声明应用所需的功能,并限定在次版本线内——ACB 和 Studio 也以相同方式写入,Introspection Service 依赖此值。<app.runtime> 保留完整补丁版本。通过上文的 <runtimeVersion>${app.runtime}</runtimeVersion> 固定值,消除了该版本下限原本可能引发的测试运行时问题。始终确保 javaSpecificationVersions 包含目标 Java 版本(若不存在,则插入该数组)。
  • 更新 mule-artifact.json(minMuleVersion、javaSpecificationVersions)
  • 更新 pom.xml(运行时版本、Java 版本、连接器版本、插件版本)
  • 原位编辑管理某个版本的任一祖先 POM(apply_parent_pom_fork.mjs --phase=edit)——在所属父级或祖父级中升级由其管理的连接器/运行时版本,祖先 <version> 保持不变,以便本地构建通过 <relativePath> 解析;已验证。<version> 分叉和子级重新指向会在第 18 步、构建通过后进行。

第 14.5 步:验证版本锁定是否生效(硬性门槛)

上述写入已完成;现在要证明 Maven 实际解析到每个连接器的目标版本,之后再花时间进行完整构建。apply_parent_pom_fork.mjs 自带的验证会重新运行静态遍历,但静态遍历存在与发现阶段相同的盲点——它无法发现导入的 BOM 或覆盖版本锁定的过时 ~/.m2。mvn dependency:tree 才是已解析值的权威依据:它回答这里唯一重要的问题——编辑是否生效? 这是第 14 步的验收测试。

# Reads tmp/target-connectors.json; runs `mvn dependency:tree` against the POMs
# as just written. Must run AFTER Step 14's writes and BEFORE the Step 16 build.
node <skill-dir>/scripts/verify_dependency_tree.mjs . --against=target

退出代码:0 表示每个连接器都解析到其 targetVersion——继续第 15/16 步;1 表示操作错误(缺少 Maven/依赖树生成失败/无法读取 target-connectors.json)——停止并解决;2 表示一个或多个版本锁定未生效。与第 12.0 步不同,这道门槛是硬性的:退出代码为 2 时,停止——不要继续构建。 使用错误版本构建可能通过打包并返回虚假的成功结果,直到部署时才暴露缺陷。

发生不匹配时,读取 tmp/dep-tree-verify-target.json,并根据 mismatches[] 采取行动:

  • 每个条目都列出连接器、expected 目标版本、Maven 实际解析出的 resolved 版本,以及 likelyCause。managedFrom 值表示某个已导入 BOM/编辑位置以外的 <dependencyManagement> 正在起作用——编辑了错误的 POM。
  • 从源头修复,然后重新运行此门槛:如果实际所属位置是被遍历过程错误标记的祖先,则更正来源信息并重新运行 apply_parent_pom_fork.mjs --phase=edit;如果是本地继承链之外导入的 BOM,则属于第 12.0 步软性门槛本应发现的同类问题——上报操作人员(无法通过编辑本地 POM 锁定该版本;必须修改 BOM 本身,或在子级中显式锁定连接器版本)。
  • resolvedOnlyByTree[]/missingFromTree[] 在此仅供参考;被阻止的连接器(没有 targetVersion)不会进行差异比较。

仅当此门槛以退出代码 0 结束后,才继续第 15/16 步(或操作人员已明确接受并记录了无法在本地修复的差异)。


第 15 步:更新应用代码

严格按照 tmp/upgrade-plan.md 中列出的编辑执行——不进行新的发现,不做新的选择。所有可能需要用户输入的事项都已在规划期间解决。逐项操作时,原样使用 references/llm-prompts.md §1 作为模型提示词(每个操作的提示词均独立完整,因此无需重新打开文件)。

全局约束(适用于以下每项编辑):

  • 保留业务意图——doc:name、DataWeave 负载、config-ref 值和错误处理器结构必须保持不变。
  • 切勿修改无关元素。
  • 切勿添加计划/Mode-B/Mode-C JSON 中未列出的操作、属性或子元素。
  • 切勿编辑 xsi:schemaLocation——该项由确定性的 apply_connector_pin.mjs(第 14 步)负责。
  • 切勿重新审议计划已作出的决定——如果某项决定看起来有误,立即停止并返回第 12 步处理。
15.1 Flow-XML 编辑(计划 §Operations + §Configs)

按操作处理 — 对计划中的每项操作:

  1. 如果该操作标记为 true-removal — operator attention required,请在编辑时使用 AskUserQuestion 确认手动重写路径。此处绝不可猜测重命名;该决策应由计划确定。
  2. 使用 Read 工具打开 usage_sites[i].file 中的 line 行(前后各读取约 10 行,以了解上下文)。
  3. 根据计划中的约定就地 Edit 元素:重命名元素(前缀 + 本地名称);根据 .attributes[].attributeName 重命名属性;根据 .childElements[] 将属性提升为子元素;移除已删除的属性;使用计划中的默认值插入新的必需属性/子元素;根据计划中的错误类型映射重写 <on-error-propagate type="…"> / <on-error-continue type="…">。
  4. 保留 doc:name、DataWeave 负载、config-ref 值及其他无关子元素。
  5. 如果命名空间前缀发生变化,请在 flow 的根 <mule> 元素上更新 xmlns:<oldprefix> → xmlns:<newprefix> 绑定。不要修改 xsi:schemaLocation。

按配置处理 — 对计划中的每个(配置、提供程序)配对:

  1. 将配置元素的本地名称重写为计划中 Mode-C 的 .elementName。
  2. 将连接元素的本地名称重写为计划中 Mode-C 的提供程序 .elementName。
  3. 根据计划重命名连接元素上的属性。
  4. 如果命名空间前缀发生变化,请重写配置元素的前缀 — 上述本地名称保持不变。

完成上述所有编辑后,验证每个已修改文件的命名空间格式正确(逐文件门禁)。 对上述按操作和按配置处理阶段中编辑的每个文件运行此验证。单独运行 xmllint --noout 不足以通过门禁 — 如果存在未绑定的命名空间前缀(例如,根元素上没有 xmlns:doc 的 doc:name),它会将其报告为警告并仍以 0 退出,因此通过退出代码 / && echo ✅ 检查的 XML 仍可能被 Mule 4.9+ 构建时 AST 解析器(mule-maven-plugin process-classes)拒绝。请在 stderr 中搜索并在发现任何命名空间错误时判定失败:

errs="$(xmllint --noout "$file" 2>&1)"
if printf '%s' "$errs" | grep -qE 'namespace error|not defined|not bound'; then
  echo "❌ $file — unbound namespace prefix:"; printf '%s\n' "$errs"; exit 1
fi

缺陷是指任何前缀元素或属性(<ee:transform>、doc:name、db:config 等)使用的前缀未在某个作用域内的祖先元素上声明 — 这是一个潜在问题,mule-maven-plugin 3.x 在构建时从不解析它,但 4.9+ 会解析。错误文本会指出出问题的前缀;请将该前缀的 xmlns:<prefix>="<namespace-uri>" 绑定添加到根 <mule> 元素(从可正常工作的 flow 文件或连接器的 Mode-A .namespace 中查找 URI)。常见情况是 doc:name 缺少 xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"。绝不可为了消除错误而删除带前缀的属性/元素。

范围限制(已知缺口): 此门禁仅检查第 15 步实际编辑的文件。计划未涉及的 flow 文件中若存在潜在的未绑定前缀,将逃过此检查并在第 16 步构建时失败。第 3.5 步的 XML 卫生门禁(阶段 1)会通过预先扫描所有 flow 文件来弥补这一缺口 — 此逐文件门禁则是对已编辑文件的第二道防线。

15.2 DataWeave 编辑(计划 §DataWeave 下游影响)

严格应用计划中列出的 DW 重写:

  • 计划建议重写的站点(Mode-B .output* 提供了同级重命名映射)→ 按照所写内容应用 Edit。
  • 标记为 SITE FLAGGED FOR OPERATOR 的站点 → 计划无法确定形状映射。请在编辑时通过 AskUserQuestion 提请确认 — 不要静默重写。

即使计划未标记,也请对连接器特有的高风险点进行合理性检查:不同驱动程序版本间 db:select 列名大小写的变化;Java 17 更严格的数字/日期强制转换(as Number、now() as String);error.errorType.identifier 字符串内容的变化(SFDC:… → SALESFORCE:… 等)。

15.3 MUnit 编辑(对应计划中的 §Operations + §DataWeave 下游影响 + §Connector-wide 错误类型重命名)

这里不处理 MUnit 版本升级。 <munit.version> 属性、munit-maven-plugin 和 munit-runner/munit-tools 依赖版本由 Step 14 中的 apply_runtime_bump.mjs(根据 .munit.to)确定性地升级。本节仅涵盖由连接器操作重命名/结构变更驱动的 MUnit 测试内容编辑——切勿在此手动编辑 MUnit 版本。

MUnit 没有专门的计划章节——其编辑内容源于流程编辑中相同的操作重命名、输出结构变更和错误类型重命名。对于计划将重写的每个操作,都要检查引用该操作的 src/test/munit/**.xml:

  • 将 <munit-tools:mock-when> 的 processor 属性更新为新元素名称(例如,processor="salesforce:query")。
  • 如果 Mode-B .output* 表示架构有所变更,则更新模拟的 <munit-tools:then-return> 负载结构。
  • 更新计划 DW 章节中标记的、读取操作响应字段的 <munit-tools:assert-that> 表达式。
  • 根据计划中的错误类型映射,更新 MUnit 错误路径中的 <munit-tools:fail> 和 <on-error-propagate type="…">。

mvn test(Step 17)是这些编辑的权威验收门槛。

15.4 自定义 Java 编辑(计划 §Custom Java 下游影响)

严格应用计划根据 Step 8a 列出的源代码改写——不作新的发现或选择(所有需要判断的事项都已在规划期间解决)。这些是对 .java 文件的常规 Edit,以及计划指定的任何依赖项添加:

  • 源代码改写——对于每条 file:line 条目,先 Read 文件以了解上下文,然后将有问题的 API Edit 为计划提出的替代方案(例如,将 javax.xml.bind.* 导入改为对应的 jakarta.* 导入,将 Class.newInstance() 改为 getDeclaredConstructor().newInstance(),将 new String(bytes) 改为明确指定字符集的形式)。保持原有行为——字符集/区域设置修复必须固定代码隐式依赖的值,而不是任意选择一个值。
  • 添加依赖项——如果计划指出已移除的 JDK 模块需要独立依赖项,则将计划指定的确切 <dependency> 坐标添加到 pom.xml。这是 Step 15 出于非版本原因修改 pom.xml 的唯一情况;这仍是计划批准的编辑,而非新发现。
  • 此处绝不编辑 8b(FLAG)条目。 仅限运行时的部分(Spring beans、Serializable 状态、安全性/TLS、log4j appenders、JDK 模块反射)已标记给操作人员处理,不在此修复。如果后续构建/测试失败涉及其中任何一项,立即停止并返回 Step 12——不要擅自改写已标记位置。

mvn clean package(Step 16)是 src/main/java 编辑的验收门槛;mvn test(Step 17)是 src/test/java 和 MUnit-Java 编辑的验收门槛。构建通过编译并不代表 8b 运行时部分已解决——这些仍是需要操作人员确认的风险。

Step 15 涉及内容摘要:

  • 根据 Step 7 分析结果,更新连接器操作变更对应的流程
  • 根据配置变更,更新配置组件
  • 根据 Step 9 分析结果,更新存在兼容性问题的 DataWeave 脚本
  • 根据 Step 8 分析结果,更新存在版本兼容性问题的自定义 Java 类
  • 根据 Step 10 分析结果,更新连接器变更对应的 MUnit 测试文件

使用 describe-connector 的元数据,确保操作、配置和属性与新版本连接器一致。


第 16 步:构建循环

有界恢复循环,最多重试 5 次。mvn clean package -DskipTests 的 BUILD SUCCESS 仅表示打包成功——它验证(a)XSD 能解析,(b)DataWeave 能编译,(c).jar 能打包。它不会执行任何流、访问任何外部系统,也不会运行 MUnit。第 17 步(mvn test)才是真正的运行时关卡。

  • 运行 JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<target-java>.json) mvn clean package -DskipTests 2>&1 | tee tmp/mvn-failures/build-<attempt>.log——将 JAVA_HOME 内联设置为目标 JDK(每次 Bash 调用都会启动一个新 shell;之前的 export 不会保留)
  • 如果 BUILD SUCCESS → 进入第 17 步。
  • 如果 BUILD FAILURE → 不要临时打补丁。 进入下面的分类器(16.1)。几乎所有 mvn 失败都是 XSD/DSL 不匹配导致的,其修复方法已包含在规划阶段(Mode-A / Mode-B / Mode-C JSON)收集的元数据中——结构化恢复会复用这些数据,而不是重新猜测。应用一个有针对性的修改,然后在新的响应中重新运行 mvn。
16.1 失败分类器——解析、分类,并根据元数据修复

解析失败信息——从 Maven 输出中提取信息(将原始输出保存到 tmp/mvn-failures/<attempt>.log):

字段出现位置示例
file[ERROR] Could not load flow: file:.../src/main/mule/<flow>.xml/…/src/main/mule/example.xml
line、colcvc-*: … [file:line:col] 或 line N column Mline 23 column 4
error_codecvc-complex-type.<N>.<N> / cvc-enumeration-valid / cvc-datatype-valid.1.2.1cvc-complex-type.3.2.2
element消息中的 <prefix:name><salesforce:basic-connection>
attribute消息中提及的属性(如果有)securityType
expected枚举/类型错误的允许值[BASIC_AUTH, OAUTH_JWT, ...]

分类为以下某一类别,并根据引用的元数据进行修复:

  1. attribute-rename — cvc-complex-type.3.2.2“Attribute '' is not allowed to appear in element 'prefix:op'”。修复源:tmp/connector-metadata/<nick>-new-<op>.json → .attributes[].attributeName。恢复方法:找到一个合理的新属性(Levenshtein 距离较近或语义角色相同)并修改对应位置;如果计划已选定映射,则按计划应用。常见情况:http:listener 的 method="X" → allowedMethods="X"(重命名仅适用于 listener 源——http:request 保留 method,因此不要修改 request 对应的位置)。
  2. missing-required-child — cvc-complex-type.2.4.a/b“Invalid content was found starting with element '…'. One of '{…}' is expected”。修复源:…-new-<op>.json → .childElements[]。恢复方法:旧属性(例如 content="#[payload]")现在变成子元素(<prefix:content>#[payload]</prefix:content>)——据此重写。
  3. element-rename — cvc-elt.1.a“Cannot find the declaration of element 'prefix:op'”。修复源:<nick>-new.json → .operations[] / .sources[]。恢复方法:如果计划已选定重命名目标,则按计划应用;否则说明计划遗漏了重命名——返回步骤 12,不要猜测。
  4. connection-provider element name — 与 (3) 相同的代码形式,但出现在 <prefix:config> 块内。修复源:…-new-<config>-<provider>.json → .elementName(config)和 .connectionProviders[] | select(.name == "<provider>") | .elementName(connection)。恢复方法:改写为 Mode-C 的 .elementName。范围明确且具有确定性——绝不要根据 SDK 标识符猜测。
  5. enum-value — cvc-enumeration-valid“Value '' is not facet-valid with respect to enumeration '[…]'”。修复源:已解析的消息包含允许值;与对应操作 JSON 的 .attributes[].allowedValues 交叉核对。恢复方法:选择映射到旧值的新枚举值(通常是重命名——BASIC → BASIC_AUTH);如果确实存在歧义,则使用 AskUserQuestion。
  6. xsi:schemaLocation URL — <mule ...> 根元素上出现 SAXException“schema_reference.4”或 cvc-elt.1.a,且 .../current/mule-<name>.xsd URL 返回 404。修复源:tmp/connector-versions/<nick>.json(固定的 GAV)以及失败连接器的固定命名空间事实依据。该事实依据是无后缀的 tmp/connector-metadata/<nick>.json,它不会预先写入(步骤 14 不再重新描述每个连接器)。现在按需重新描述这一个失败的连接器——需要 Java 17+,与步骤 7 相同。内联固定 JDK(这是一次性调用,不是步骤 7 的扇出;每次 Bash 调用都会启动一个全新的 shell——先前的 export 可能未生效);脚本本身会拒绝使用低于 Java 17 的版本:
    JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<target-java>.json) node <skill-dir>/scripts/describe_connector.mjs "<nick>"        # no -new suffix → writes tmp/connector-metadata/<nick>.json
    
    然后读取 <nick>.json → .namespace.uri。恢复方法:重新运行 apply_connector_pin.mjs <nick> .(它会读取 <nick>-new.json 中的命名空间,并确定性地重写 URL)。不要手动编辑 xsi:schemaLocation。
  7. pom / plugin / runtime — 应用加载前来自 reactor 的任何问题(找不到 mule-maven-plugin、${app.runtime} 未解析、javaSpecificationVersions 不匹配、本地仓库中缺少构件)。修复源:tmp/upgrade-targets.json(.muleMavenPlugin.to,步骤 11a 的 MMP);pom.xml + mule-artifact.json;tmp/connector-versions/*.json。恢复方法:重新运行 apply_runtime_bump.mjs .(runtime/plugin 属性错误)或 apply_connector_pin.mjs <nick> .(依赖版本错误)。只有当两个脚本都报告 not-found 时,才手动编辑 pom.xml。
  8. unknown / other — 不符合上述类别。修复源:结合 Mule 4 XSD/DSL 语义,交叉核对 tmp/connector-metadata/*.json + tmp/connector-versions/*.json。恢复方法:重新检查失败所指向的先前阶段的输出;将推理记录到 tmp/mvn-failures/<attempt>.log。只有在 5 次重试额度用尽,或同一项基于知识的编辑已失败过一次时,才转而使用 AskUserQuestion。 每次重试只进行一项针对性编辑(一次 Edit,或恢复步骤中指定的那次脚本重跑)——不要批量修改。在新的响应中重新运行 mvn clean package。成功 → 步骤 17;失败 → 重试计数加 1 并重新解析。
诊断升级阶梯——不透明故障时必须执行

只读取简短的 mvn 报错行就进行“猜测-修复-重试”,当错误消息未指出出错文件时会浪费重试次数。以下两种故障模式尤其不透明,必须在下一次代码编辑之前执行升级探测——不能等到浪费一次重试之后。

触发条件 A——固定版本的连接器的操作/元素出现 XSD “invalid content” 错误。 症状:cvc-complex-type.2.4.a: Invalid content was found starting with element '<ns>:<op>'。 含义:流程 XML 调用了一个操作,而实际解析到的 XSD 并未声明该操作。pom 中的固定版本可能未生效(本地仓库缓存过期、父 POM 中存在传递性固定版本、current/ 别名解析到旧版本)。

进行任何编辑之前,先运行:

mvn dependency:tree -Dincludes=<groupId>:<artifactId> 2>&1 | tail -20
  • 如果实际解析到的版本 ≠ 固定版本 → rm -rf ~/.m2/repository/<groupPath>/<artifactId>/<staleVer>,然后使用 -U 重新运行 mvn ...。
  • 如果版本匹配 → 该操作确实已重命名。重新读取连接器的 Mode-B / <nick>-op-renames.json,并将新名称应用到流程 XML。

触发条件 B——不透明的 ClassCastException / NullPointerException,堆栈中没有应用文件。 症状:java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer(或类似错误),且堆栈跟踪完全位于 org.mule.runtime.* / com.mulesoft.* 类中——没有 src/main/mule/ 下的文件/行号。

进行任何编辑之前,使用调试模式重新运行并 grep 故障帧和 bean 上下文:

mvn clean package -DskipTests -X 2>&1 | grep -B 20 -A 3 'ClassCastException\|NullPointerException' \
  | tee tmp/mvn-failures/build-<attempt>-debug.log

然后扫描前面的 20 行,查找 Creating bean / parsing element / BeanDefinition——这些内容会指出正在构造的流程 XML 元素(例如 db:pooling-profile、http:listener-connection)。修复应针对该元素。常见原因:${property} 占位符用于某个属性,而新版连接器现在将该属性类型定义为 xs:int / xs:boolean——根据 Mode-B .attributes[].type 移除引号,或用 ${int(...)} 包裹。

两种探测都很快(只需调用一次 mvn,不更改代码),并且必须在第三次重试之前运行——否则,循环会在仍然猜测的情况下达到 5 次重试上限。

触发条件 C——XSD 错误涉及计划中已预先处理的元素/属性。 症状,符合以下任一情况:

  • cvc-complex-type.2.4.a: Invalid content ... element '<ns>:<name>',且 grep 显示 tmp/upgrade-plan.md 中有 Rewrite <ns>:<name> →;或
  • cvc-complex-type.4: Attribute '<attr>' must appear on element '<ns>:<name>',且 grep 显示 tmp/upgrade-plan.md 中有 Add <ns>:<name> @<attr>=。

不要根据 XSD 错误文本重新分析,也不要猜测枚举值。使用 grep -A2 "Rewrite <ns>:<name>\|Add <ns>:<name>" tmp/upgrade-plan.md 查找目标指令,然后逐字应用该编辑。计划是在已有 Mode-B/C 元数据的基础上编写的(步骤 12 子步骤 2)——请相信它。如果计划条目缺失,但步骤 11.5 中出现了 WARN(或 Mode-B/C JSON 中将该属性声明为 required: true),则说明步骤 12 被跳过;返回步骤 12 并重新综合计划(不要在步骤 16 中用猜测敷衍过去)。

重试上限与停止
  • 重试预算:最多 5 次构建失败(日志记录和诊断轮次计为 ½ 次重试——参见 16.1 中的分类器)。
  • 如果在运行两种升级探测后,经过 5 次实际编辑重试仍然 BUILD FAILURE,则通过 AskUserQuestion 停止,并提供:
    1. 最近三个 tmp/mvn-failures/build-<N>.log 的前 30 行
    2. 依赖树摘录(触发条件 A)或调试堆栈帧(触发条件 B),以实际运行的探测为准
    3. 每次重试应用的分类
    4. 2–4 个后续操作候选方案(通常包括:固定另一个连接器版本、撤销一项流程 XML 编辑、请求对失败配置进行 Mode-C describe)

步骤 17:MUnit 循环

仅在步骤 16 报告 BUILD SUCCESS 后运行。mvn clean package 只验证打包;MUnit 才是权威的运行时门禁。

只有在插件已接入且至少有一个测试套件可运行时,MUnit 才会运行。两项都要检查——下方循环的通过门禁会将缺少 MUnit Run Summary 视为 FAIL,因此在没有测试套件时进入循环,会耗尽所有重试次数,并在应用正常的情况下触发 HALT。

grep -c 'munit-maven-plugin' pom.xml                    # plugin wired?
find src/test/munit -name '*.xml' 2>/dev/null | wc -l   # suites present?
  • 插件数量为 0 → 未接入 MUnit。记录 no runtime validation performed — no munit-maven-plugin declared 并跳过循环。
  • 插件数量 >= 1,但测试套件数量为 0 → 已接入 MUnit,但没有可运行的测试套件(mvn clean test 会打印 "no MUnit suites found",且不会生成 MUnit Run Summary)。这不是失败。记录 no runtime validation performed — munit-maven-plugin declared but no suites under src/test/munit 并跳过循环。
  • 插件数量 >= 1,且测试套件数量 >= 1 → MUnit 有可运行的测试套件。进入循环。

优先使用 mvn clean test,而不是单独使用 mvn test。 clean 可减少复用过期缓存,但并不能保证 Skipping execution of munit because it has already been run 这行消失——MUnit 会在 reactor 中多次绑定其 test 执行,因此真实运行和后续的 Skipping… 行通常会同时出现在同一份日志中。单独出现 Skipping… 不是失败。

通过门禁只看 MUnit Run Summary,其他内容一概不算——既不是 BUILD SUCCESS,也不是没有 Skipping…。 对整份日志进行 Grep(grep -nE 'MUnit Run Summary|Tests run:' <log>),绝不要只根据日志末尾判断。只有日志中包含实际的 MUnit Run Summary,且显示 Tests run: N (N >= 1)、Failed: 0、Errors: 0 时,MUnit 才算通过。只有在完全没有 MUnit Run Summary、显示 Tests run: 0,或 Failed/Errors > 0 时,才视为 FAIL——即使构建显示 BUILD SUCCESS 也一样。如果日志末尾有 Skipping execution of munit,但前面有实际的 MUnit Run Summary,则算通过。

每次回复只运行一次 mvn clean test。 失败时,采用与步骤 16 分类器(16.1)相同的恢复方法,但适用类别范围更窄——MUnit 失败总是在测试 XML 中,而不在 flow XML 中。将每次失败运行的输出保存到 tmp/mvn-failures/munit-<attempt>.log。MUnit 特有的五种类别:

  1. attribute-rename:针对 <munit-tools:mock-when processor="<prefix:op>"> ——存在未知的 mock 属性。修复依据:tmp/connector-metadata/<nick>-new-<op>.json 中的 .attributes[].attributeName。
  2. element-rename:针对 <munit-tools:mock-when processor="<prefix:oldOp>"> ——processor 属性引用的 op 已不存在。修复依据:<nick>-new.json 中的 .operations[];应用计划中的重命名映射。
  3. connection-provider element name ——形态与 element-rename 相同,但位于 <munit-tools:mock-when processor="<prefix:config>"> 中,引用的是 connection-provider 元素。修复依据:…-new-<config>-<provider>.json 中的 .elementName。
  4. enum-value:针对被 mock 的 payload ——<munit-tools:then-return> 返回的常量已不是有效的枚举值。修复依据:各 op 的 .attributes[].allowedValues。
  5. assertion-shape ——<munit-tools:assert-that> 读取了 NEW op 不再输出的字段。交叉参考 Mode-B 的 .output* 键,重写 JSONPath / DW 读取内容。

MUnit 失败与 flow-XML 失败一样,根据 Mode-B 的 .attributes[] / .childElements[] / .output* JSON 进行分类。

重试预算:6 次。 MUnit 失败比 XSD/DSL 失败更分散(测试编写风格各异,而且一次 op 变更往往会影响多个 mock),因此重试预算比步骤 16 的 5 次上限更宽松。第 6 次 mvn clean test 失败后,通过 AskUserQuestion 触发 HALT,并提供最近三次 tmp/mvn-failures/munit-<attempt>.log 的摘录(每份前 30 行)、分类结果、已应用的修改,以及 2–4 个候选后续操作。 未经用户指示,不要尝试第 7 次重试。将反复失败视为计划遗漏了 Mode-B / Mode-C 细节的信号,而不是可以忽略并继续重试的噪声。

  • 运行 JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<target-java>.json) mvn clean test——像步骤 3c 和 16 一样,在命令中将 MUnit 运行固定到已解析的目标 JDK
  • 确认 MUnit Run Summary 显示 Tests run: N (>= 1)、Failed: 0、Errors: 0——仅有 BUILD SUCCESS 并不代表通过
  • 修复 MUnit 测试
  • 重复以上步骤,直到所有测试通过

步骤 18:分叉 Parent-POM 版本

**条件:**仅当步骤 14 的 apply_parent_pom_fork.mjs --phase=edit 报告了一个或多个所属上级时才适用,即其输出 ancestorsForked[] 非空。如果应用仅有子级,编辑阶段会打印 "nothing to do",无需分叉;请跳到步骤 19。

此时,针对步骤 14 原地写入各所属上级的新连接器/运行时版本,构建已通过(步骤 16/17 已通过);验证方式是使用子级中未更改的 <parent> 链接和 <relativePath>。本步骤执行分叉:提升各所属上级自身的 <version>,并重新指定下游 <parent><version>,让此应用的链重新关联到新的上级版本。

**首先,询问操作员要采用哪一级版本提升。**不要擅自决定。先以试运行模式执行分叉,获取各所属上级的原始 <version>,不写入任何内容:

node <skill-dir>/scripts/apply_parent_pom_fork.mjs . --phase=fork --dry-run

此命令会打印简短摘要;完整结果写入 tmp/parent-pom-fork-dryrun.json。从该文件读取 ancestorsForked[](jq '.ancestorsForked' tmp/parent-pom-fork-dryrun.json)。每个条目包含 ownVersion.from(此分叉计算所依据的原始基准版本)、artifactId 和 pomPath(字段名是 pomPath,不是 path;每个条目没有 edits 键——编辑日志是顶层唯一的 edits[])。对每个上级,根据 ownVersion.from 计算以下三个 semver 候选版本:

  • Major — x.0.0,将 x 加 1(例如,3.2.0 → 4.0.0);适用于有破坏性的连接器升级。
  • Minor — x.(y+1).0(例如,3.2.0 → 3.3.0);普通连接器/运行时升级的默认选项。
  • Patch — x.y.(z+1)(例如,3.2.0 → 3.2.1);适用于微小升级。

然后通过一次 AskUserQuestion 提供这三个级别(Minor 放在首位并建议优先选择)。选定的级别适用于所有已分叉的上级——提升级别是整个升级的属性,而不是每个 POM 各自的属性——但应在每个选项中显示各上级对应的具体版本号,让操作员确切了解每个级别会产生什么结果。对于包含两个上级的链,Minor 选项的说明可以写成例如 "grandparent 1.0.0→1.1.0, parent 3.2.0→3.3.0"。仅当 ancestorsForked[] 非空时才提示(上面的步骤 14 条件已确保这一点)。

然后,使用操作员选择的级别正式执行分叉:

node <skill-dir>/scripts/apply_parent_pom_fork.mjs . --phase=fork --fork-bump=<chosen>   # major | minor | patch

此命令会打印简短摘要;完整结果写入 tmp/parent-pom-fork.json(如需查看各上级的详细信息,请使用 jq 读取)。该命令会按链路由深至浅处理(提升祖父级 <version> → 重新指定父级的 <parent> 引用 → 提升父级 <version> → 重新指定子级的 <parent> 引用),并在退出前验证每个下游链接都指向分叉版本。根据输出,逐个已分叉的上级列出:

  • 上级 POM 路径及其 <version> 变更(例如,all-scenarios-grandparent 1.0.0 → 1.1.0);
  • 其中已提升版本的连接器/属性(已在步骤 14 中应用);
  • 任何需要操作员注意的警告(上级中声明的连接器没有已解析的目标——未提升版本,保持原样)。 通过重新构建确认。 分叉是此工作流中的最后一次文件修改,而脚本的链接检查是静态验证(它会确认每个下游 <parent><version> 都指向分叉版本),并非编译。由于其他每个会修改文件的步骤最后都有构建门槛,也为这一步补上构建门槛——按照第 16 步的方式,严格指定目标 JDK,再运行一次构建,并在清理前要求结果为 BUILD SUCCESS:
JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<target-java>.json) mvn clean package

这可以捕获静态验证通过、但实际无法解析的版本重定向问题(例如,<relativePath> 未指向已分叉的祖先,或同级模块的 GAV 不匹配)。如果失败,请修复 <parent>/<relativePath> 链接——不要手动编辑版本(见下文的幂等性说明)——并在继续第 19 步之前重新运行。

分叉版本号具有幂等性。 分叉的 <version> 始终根据第 14 步 tmp/pom-backups/ 快照中捕获的祖先原始版本计算,而不是根据当前文件计算。因此,如果操作者改变主意,只需用不同的 --fork-bump 级别重新运行(无需手动恢复):再次运行 --fork-bump=minor 会再次得到 1.0.0 → 1.1.0,而不是 1.1.0 → 1.2.0;切换到 major 时,则会从原始版本重新计算为 1.0.0 → 2.0.0。不要手动编辑祖先的 <version> 或子模块的 <parent><version>——脚本会让两者保持一致并重新验证;手动编辑会破坏这一不变条件。

这些是本地修改——此技能不会发布任何内容。 分叉只会修改磁盘上的文件:祖先 POM 的 <version> 和子模块的 <parent> 引用。请明确告知操作者:父级/祖父级 POM 现在使用了一个新的本地版本,其他地方尚未识别此版本。如果同级应用通过 GAV 使用此父 POM(例如从 Exchange 或 Maven 仓库获取),操作者可以自行发布新的祖先版本,以便这些同级应用解析该版本——此技能不会代替他们运行任何 anypoint-cli / mvn deploy 发布操作。在他们发布之前,此应用会通过 <relativePath> 在本地基于磁盘上的分叉版本构建,而同级应用仍会照常解析旧的、已发布的祖先版本。


第 19 步:清理工作区 tmp/

仅在第 16 步报告 BUILD SUCCESS 且第 17 步记录了 MUnit 判定后,才能删除 tmp/——这些状态文件有助于诊断失败,因此不要在流程中途清理。

分两次单独响应执行这两项删除操作,每次只执行一个 rm——不要将它们串联到同一条命令中,也不要与 mvn 或完成信号打包执行。这两个目标都已加入 gitignore,因此即使删除被拒绝也无妨。

rm -r tmp/

然后,在单独的响应中,删除第 7 步临时安装的 fast-xml-parser。请解析安装时使用的同一物理路径(pwd -P),确保 rm -rf 指向软件包实际安装的目录:

rm -rf "$(dirname "$(cd "<skill-dir>" && pwd -P)")/node_modules"

步骤 20:宣布完成

单独回复。 不要运行 mvn、rm 或调用其他工具。本回复只能包含三行摘要。前置条件:

  1. 步骤 16 最后一次在升级后的项目上返回了 BUILD SUCCESS。
  2. 步骤 17 要么记录了 mvn test 通过,要么写下了 no runtime validation performed — … 跳过说明(没有插件,或插件已接入但没有测试套件)。
  3. 步骤 19 的两项清理操作(rm -r tmp/,然后 rm -rf 删除临时 node_modules)都已分别在之前的回复中执行。

严格输出三行:

  1. BUILD SUCCESS,并附上 target/<project>-*.jar 的路径。
  2. 步骤 17 的 MUnit 结果(mvn test: all passing 或步骤 17 记录的确切 no runtime validation performed — … 跳过说明:没有插件,或插件已接入但 src/test/munit 下没有测试套件)。
  3. 一行版本变更摘要:Mule <from> → <to>, Java <from> → <to>, connectors: <C> of <total> updated——在步骤 19 删除文件之前,从 tmp/upgrade-targets.json(.mule.from/.mule.to、.java.from/.java.to)读取 from/to 值,或使用步骤 4c 中锁定的目标版本。<C> 是实际变更了版本的连接器数量(target-connectors.json .connectors[] | changed == true),<total> 是范围内的所有连接器——因此,已经是最新版本的连接器(例如 email 1.8.0 → 1.8.0,changed == false)计入 <total>,但不计入 <C>,所以不能让数字看起来像所有连接器都升级了。在步骤 19 删除 tmp/ 之前,从 target-connectors.json 中记录这两个数量。当 <C> 等于 <total>(全部都发生了变更)时,结果自然显示为 N of N updated。

不要包含逐文件差异、“已完成事项”回顾或推测性的“后续步骤”——用户可以查看差异。

最终摘要须包含:

  • 已达到的目标版本(Java、Mule Runtime)
  • 已更新的连接器(数量和版本)
  • 构建状态:SUCCESS
  • 测试状态:ALL PASSING
  • 后续步骤:审查变更、提交、部署

故障排除

找不到 anypoint-cli-v4: npm install -g @mulesoft/anypoint-cli-v4

找不到 DX 插件: npm install -g @salesforce/anypoint-cli-dx-mule-plugin

需要 Runtime 路径: 首次使用 dx mule describe-connector 或相关命令时,会提示输入 Runtime 位置。该路径会保存到 ~/.mule-dx/config.json。

父 POM 不可用: 必须能在本地访问父 POM,才能解析继承的版本。不要尝试下载;请让用户在本地提供该文件,然后重新运行步骤 2。

Exchange 中没有该连接器: 无法自动升级。将其标记为阻碍事项并通知用户。

更新版本后构建失败: 查看 describe-connector 输出中的连接器操作变更。更新 flow XML,使其符合新的操作签名。

MUnit 测试失败: 更新测试模拟和断言,使其符合新的连接器操作签名和响应结构。


快速参考

以下 <skill-dir> 是在“skill is now active”消息中提供给你的绝对路径。请始终使用该路径——不要构造相对的 ../scripts/... 路径。

# Step 1 — validate prerequisites (writes tmp/upgrade-prereqs.json; non-zero exit => STOP)
node <skill-dir>/scripts/validate_prerequisites.mjs .

# Step 2a — detect current Mule Runtime version (writes tmp/current-mule-version.json)
node <skill-dir>/scripts/detect_current_mule_version.mjs .
# When Step 2a needs a prompt (or Step 3a correction): persist the user's answer (sets belowFloor if < 4.3)
node <skill-dir>/scripts/detect_current_mule_version.mjs . --user-version <v>

# Step 2b — detect current Java version (writes tmp/current-java-version.json)
node <skill-dir>/scripts/detect_current_java_version.mjs .
# When Step 2b needs a prompt (or Step 3a correction): persist the user's answer
node <skill-dir>/scripts/detect_current_java_version.mjs . --user-version <n>

# Step 3b — ensure the current Java JDK is available (writes tmp/resolve-jdk-<major>.json)
# MAY download over the network; pass --no-download to only detect an installed JDK.
node <skill-dir>/scripts/resolve_jdk.mjs <current-java-major> .

# Step 3c — detect current MMP version (writes tmp/current-mmp.json; needsPluginBump=true when MMP is 3.x)
# 3.x can't run on the required Maven 3.9.x → Step 3c bumps MMP to latest 4.x for the baseline build only.
node <skill-dir>/scripts/detect_current_mmp_version.mjs .

# Step 3c — baseline build on the resolved JAVA_HOME (must be BUILD SUCCESS)
JAVA_HOME=$(jq -r .javaHome tmp/resolve-jdk-<major>.json) mvn clean package

# Step 4 — recommend a target (writes tmp/target-versions.json)
node <skill-dir>/scripts/resolve_target_versions.mjs .

# Step 4 — validate a user-requested target (only when the user named one; never invent one)
TARGET_MULE=<user-requested-mule> node <skill-dir>/scripts/resolve_target_versions.mjs .

# Step 5a — extract connector deps from the POM + local parent chain
# (writes tmp/connectors.json; advisory, always exits 0 — branch on connectors[]/needsUserPrompt)
node <skill-dir>/scripts/extract_connectors.mjs .

# Step 5b — check the CURRENT version's Java support in Exchange
# (writes tmp/connector-java-compat.json; non-zero exit => STOP)
node <skill-dir>/scripts/check_connector_java_compat.mjs .

# Step 6 — resolve the LATEST target-compatible version per connector
# (reads the target from tmp/target-versions.json options[0]; writes tmp/target-connectors.json; non-zero exit => STOP)
node <skill-dir>/scripts/resolve_target_connectors.mjs .

# Step 6 — resolve against a user-confirmed target instead of options[0]
# (use the confirmed pair from tmp/target-versions.json — never invent versions)
TARGET_MULE=<confirmed-mule> TARGET_JAVA=<confirmed-java> node <skill-dir>/scripts/resolve_target_connectors.mjs .

# Step 6.5 — bridge: write tmp/connector-choices/<nick>-new.json + tmp/upgrade-targets.json (Write/jq), then sanity-check
jq -e '.mule.to and .java.to and (.connectors|type=="array")' tmp/upgrade-targets.json

# Step 7 prereq — register a Mule >=4.9 runtime substrate for describe (reuse mule.to if >=4.9)
SUBSTRATE=$(jq -r 'if (.mule.to | split(".") | map(tonumber)) as $v | ($v[0] > 4 or ($v[0] == 4 and $v[1] >= 9)) then .mule.to else "4.9.19" end' tmp/upgrade-targets.json)
node <skill-dir>/scripts/resolve_runtime.mjs "$SUBSTRATE" .
anypoint-cli-v4 dx mule runtime path --set "$(jq -r .runtimePath tmp/resolve-runtime-$SUBSTRATE.json)"

# Step 7a — Mode-A summary describe of a NEW connector version (Java 17+ required)
node <skill-dir>/scripts/describe_connector.mjs <nick>-new

# Step 7b — enumerate connector usage from flow XML (parser-preferred, grep fallback rc=3)
node <skill-dir>/scripts/enumerate_usage_xml.mjs <nick> .
node <skill-dir>/scripts/enumerate_usage.mjs <nick> .

# Step 7b — Mode-B (per op/source) and Mode-C (per config-provider) describe
node <skill-dir>/scripts/describe_connector.mjs <nick>-new --type operation --name <op>
node <skill-dir>/scripts/describe_connector.mjs <nick>-new --type connection-provider --name <provider> --config-name <config>

# Step 11.5 — coverage gate (exit 1 => re-run missing describes)
node <skill-dir>/scripts/verify_metadata_coverage.mjs

# Step 12.0 — SOFT gate: cross-check discovery vs Maven (exit 2 => annotate plan + raise at approval, don't halt)
node <skill-dir>/scripts/verify_dependency_tree.mjs . --against=existing

# Step 13 — ensure the TARGET JDK + Mule Runtime are present (check local dir, else download)
# both read the finalized target from tmp/upgrade-targets.json; non-zero exit => surface stdout & WAIT
node <skill-dir>/scripts/resolve_jdk.mjs "$(jq -r '.java.to' tmp/upgrade-targets.json)" .
node <skill-dir>/scripts/resolve_runtime.mjs "$(jq -r '.mule.to' tmp/upgrade-targets.json)" .

# Step 14 — deterministic version rewrites
node <skill-dir>/scripts/promote_new_connector_pins.mjs
node <skill-dir>/scripts/apply_runtime_bump.mjs .
node <skill-dir>/scripts/apply_connector_pin.mjs <nick> .

# Step 14.5 — HARD gate: verify pins took before the build (exit 2 => STOP, don't build)
node <skill-dir>/scripts/verify_dependency_tree.mjs . --against=target

评论

支持 Markdown