项目替换 CLI
在 CI/CD 中无需 Git 即可跨 Activepieces 部署镜像项目
ap project replace CLI 通过直接 API 调用,将工作流、表格 schema 和文件夹从一个 Activepieces 部署镜像到另一个。当基于 Git 的项目发布不适用时,可以在 CI/CD 中使用它在独立的暂存和生产实例之间推广工作。
先决条件
- 目标平台的计划必须启用环境功能——参见项目发布先决条件。
- 你在两个实例上都需要一个平台范围的 API 密钥(
SERVICE主体)。两侧必须使用相同的项目 ID 格式。 - 两个部署必须共享相同的 Activepieces 主版本,且目标版本必须大于或等于源版本。
镜像内容
| 资源 | 行为 |
|---|---|
| 工作流 | 通过 externalId 在目标上创建/更新/删除 |
| 表格 schema | 仅 schema——字段、名称、externalId。行数据从不复制 |
| 文件夹 | 通过 externalId 镜像 |
| 必需 pieces | 在目标上自动安装源的确切固定版本(官方 + 自定义 npm pieces)。如果任何安装失败,替换操作会在其他写入之前中止 |
| 连接 | 元数据自动镜像(externalId、pieceName、displayName);密钥值从不通过网络传输。新连接以 status: MISSING 占位符形式落在目标上。操作员在目标 UI 中逐个授权后工作流才能运行 |
| MCP 服务器、代理、项目元数据、自定义域名、应用凭证 | 不在范围内。不做任何修改 |
安装
npm install -g activepieces
activepieces CLI 随每个 Activepieces 版本发布;将版本固定为与目标实例匹配的版本,以确保请求结构一致。
命令
# 通过环境变量设置 API 密钥(推荐用于 CI——将密钥保留在进程参数和 shell 历史之外)
export AP_SOURCE_API_KEY="$STAGING_API_KEY"
export AP_DEST_API_KEY="$PROD_API_KEY"
ap project replace \
--source-url https://staging.activepieces.com \
--source-project "$STAGING_PROJECT_ID" \
--dest-url https://prod.activepieces.com \
--dest-project "$PROD_PROJECT_ID" \
[--json]
参数
| 参数 | 必需 | 用途 |
|---|---|---|
--source-url | 是 | 源实例的基础 URL(无需尾随斜杠) |
--source-api-key | 标志或环境变量 | 源的平台 API 密钥。回退到 AP_SOURCE_API_KEY |
--source-project | 是 | 源实例上的项目 ID |
--dest-url | 是 | 目标实例的基础 URL |
--dest-api-key | 标志或环境变量 | 目标的平台 API 密钥。回退到 AP_DEST_API_KEY |
--dest-project | 是 | 目标实例上的项目 ID |
--json | 否 | 输出机器可读的 JSON 而非人类可读的摘要 |
退出码
| 码 | 含义 |
|---|---|
0 | 应用成功;每个项目都干净地应用 |
1 | 应用成功但至少有一个项目失败。检查响应中的 failed[] |
2 | 服务端预检失败(422)。未发生任何写入 |
3 | 服务端中止:piece 安装失败(502)、通用 5xx,或目标项目上已有另一个替换正在进行(409) |
4 | 本地 CLI/传输错误——URL 错误、主机不可达、API 密钥无效 |
运行时的行为
- CLI 通过源实例的 REST API 列出源项目的工作流、文件夹和表格 schema。
- CLI 将它们打包成
ProjectReplaceRequest并通过POST发送到目标的/v1/projects/:projectId/replace。 - 目标 获取每个项目的
NoWait锁。如果另一个替换正在进行,则返回409 REPLACE_IN_PROGRESS。 - 目标预检(以下任一失败则不会写入):
- 解析 Activepieces 版本;相同主版本且
dest >= source - 对于每个源连接:如果目标已有相同
externalId的连接,其pieceName必须匹配
- 解析 Activepieces 版本;相同主版本且
- 安装阶段——对于
requiredPieces中目标尚未安装确切固定版本的每个条目,服务器从 npm 安装(packageType: REGISTRY,范围:平台)。尝试所有安装;如果有任何失败,响应以502中止并列出每个失败。此时尚未发生其他写入——文件夹、表格、工作流和连接仍未被触及。 - 应用阶段——先连接,然后文件夹,然后表格,然后工作流被创建/更新。删除按反向顺序执行。每个项目在自己的 try/catch 中运行;逐项失败被收集,系统性 5xx 错误中止运行。
- 审计事件
project.replaced仅在应用阶段运行时发出——在SUCCESS或PARTIAL_FAILURE时。被拒绝的尝试(预检失败、安装失败、锁冲突)不会被审计。
输出
人类可读(默认)
替换完成,耗时 1240ms
pieces : 已安装 1 个
flows : 创建 1 个,更新 2 个,删除 0 个,未变动 47 个
tables : 创建 0 个,更新 0 个,删除 0 个,未变动 5 个
folders : 创建 0 个,更新 1 个,删除 0 个,未变动 3 个
connections : 创建 1 个,更新 0 个,未变动 4 个
目标上已安装 1 个 piece(s):
- activepieces-onlinepay@0.0.7 (CUSTOM)
目标上有 1 个 connection(s) 需要授权后工作流才能运行:
- Slack Main (@activepieces/piece-slack) [externalId=slack_main]
--json
{
"applied": {
"flowsCreated": 1, "flowsUpdated": 2, "flowsDeleted": 0, "flowsUnchanged": 47,
"tablesCreated": 0, "tablesUpdated": 0, "tablesDeleted": 0, "tablesUnchanged": 5,
"foldersCreated": 0, "foldersUpdated": 1, "foldersDeleted": 0, "foldersUnchanged": 3,
"connectionsCreated": 1, "connectionsUpdated": 0, "connectionsUnchanged": 4
},
"failed": [
{ "kind": "flow", "externalId": "...", "op": "UPDATE", "error": "..." }
],
"piecesInstalled": [
{ "name": "activepieces-onlinepay", "version": "0.0.7", "pieceType": "CUSTOM" }
],
"connectionsAwaitingAuthorization": [
{ "externalId": "slack_main", "pieceName": "@activepieces/piece-slack", "displayName": "Slack Main" }
],
"durationMs": 1240
}
预检失败(退出码 2)
{
"errors": [
{ "kind": "AP_VERSION_MISMATCH", "sourceVersion": "1.2.0", "destVersion": "1.1.5", "message": "..." },
{ "kind": "CONNECTION_PIECE_MISMATCH", "externalId": "slack_main", "expectedPieceName": "@activepieces/piece-slack", "foundPieceName": "@activepieces/piece-discord" }
]
}
完整的预检错误类型:AP_VERSION_MISMATCH、PIECE_VERSION_MISMATCH、CONNECTION_PIECE_MISMATCH。
安装失败(退出码 3,HTTP 502)
{
"failures": [
{ "pieceName": "@activepieces/piece-slack", "version": "1.2.3", "pieceType": "OFFICIAL", "message": "ENGINE_OPERATION_FAILURE: ..." },
{ "pieceName": "pdfcrowd-piece-activepieces", "version": "0.0.5", "pieceType": "CUSTOM", "message": "..." }
]
}
发生这种情况时,工作流/表格/文件夹/连接尚未被触及——运行在应用阶段前已中止。常见原因:该版本的 piece 不在 npm 上、npm 注册表不可达、引擎在元数据提取期间崩溃、目标平台上缺少用于私有包的 npm 认证令牌。
幂等性与重试
在部分失败后重新运行 CLI 会收敛到源状态。前一次运行已应用的项目通过类型深度相等检查检测为未变动并跳过;失败的项目被重试。在硬失败时,目标会处于部分应用状态——这是有意设计的,因为暂停/恢复会迫使每次成功发布都停机。
应用阶段不包装在单个数据库事务中,这是经过深思熟虑的:
- 部分成功语义(
207+failed[])要求成功应用的项目在兄弟失败时仍然持久存在。单个事务会因为一个工作流的重新发布失败而回滚每个成功的文件夹/表格/连接。 - 工作流重新发布会调度 BullMQ 作业和触发源注册,这些无法放在 SQL 事务中。
- 恢复通过重新运行而非回滚实现。每个操作通过
externalId匹配,因此进程在应用阶段崩溃会使目标处于某种中间状态,下一次运行的差异检测阶段会找到并完成它。CI/CD 的自然重试机制处理进程级失败(OOM、SIGKILL、部署超时)。
如果你的 CI 确实需要工作流级别的”全有或全无”属性,请在作业步骤中驱动替换,在非零退出时重试,且仅将退出码 0 视为成功。
连接
连接元数据(externalId、pieceName、displayName)作为占位符记录镜像到目标,状态为 status: MISSING。密钥值(OAuth 令牌、API 密钥等)从不通过网络传输——每个实例各自保管。
替换后,CLI 会打印出目标上仍需要授权的连接:
目标上有 1 个 connection(s) 需要授权后工作流才能运行:
- Slack Main (@activepieces/piece-slack) [externalId=slack_main]
操作员打开目标 UI → 连接 → 使用目标的凭据(不同的 Slack 工作区、生产级别的 API 密钥等)重新连接每个占位符。在此完成之前,任何使用该连接的工作流运行都会在执行时失败。授权后重新运行 CLI 对于已经是 ACTIVE 状态的连接是空操作。
如果目标上已存在具有相同 externalId 但不同 pieceName 的连接,替换会在预检时失败,返回 CONNECTION_PIECE_MISMATCH,以便在不会覆盖不相关的工作流的前提下解决冲突。
GitHub Actions 示例
name: 从暂存推广到生产
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
replace:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g activepieces
- run: |
ap project replace \
--source-url "${{ vars.STAGING_URL }}" \
--source-project "${{ vars.STAGING_PROJECT_ID }}" \
--dest-url "${{ vars.PROD_URL }}" \
--dest-project "${{ vars.PROD_PROJECT_ID }}" \
--json
env:
AP_SOURCE_API_KEY: ${{ secrets.STAGING_API_KEY }}
AP_DEST_API_KEY: ${{ secrets.PROD_API_KEY }}
非零退出码会导致作业失败——预检错误、服务器错误或传输错误都会自然地传递给 GitHub Actions,无需额外处理。