Skip to main content
GeneralTestAny-io

testany-sync-case-env-from-source

从 OpenAPI 生成的 Testany case 源代码中自动识别入参和出参,并同步到 case 的环境变量列表中。适用于源代码存储在需要认证的 Git 仓库(如 Bitbucket)的场景。

Stars
73
Source
TestAny-io/testany-agent-skills
Updated
2026-05-06
Slug
TestAny-io--testany-agent-skills--skills
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/TestAny-io/testany-agent-skills/HEAD/plugins/testany-bot/skills/SKILL.md -o .claude/skills/skills.md

Drops the SKILL.md into .claude/skills/skills.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

Testany Case 环境变量同步 Skill

本 Skill 用于从 OpenAPI definition 生成的 Testany case 源代码中自动提取入参和出参,并将它们同步更新到 Testany case 的 environment_variables 配置中。

适用场景

当 Testany case 的源代码满足以下条件时使用本 Skill:

  • 源代码由 OpenAPI definition 自动生成
  • 源代码包含 BASE_STRUCTURES_INFOrelay_request_fields 等特征结构
  • 源代码存储在需要认证的 Git 仓库(如私有 Bitbucket 仓库)
  • 需要将代码中的 ENV[...] 变量和出参字段同步到 case 配置

用户交互流程

步骤 1:选择操作模式

用户首先选择操作模式:

模式 说明
单 Case 操作 直接输入一个 case key
批量 Keys 操作 输入多个 case key(逗号分隔)
搜索批量操作 通过条件搜索 case,对结果进行批量操作

使用 AskUserQuestion 工具让用户选择模式。


步骤 2:获取 Case 列表

根据用户选择的模式,获取待处理的 case 列表:

模式 A:单 Case 操作

case_keys = [user_input_case_key]

模式 B:批量 Keys 操作

# 解析逗号分隔的 keys
case_keys = user_input_keys.split(',')
case_keys = [key.strip() for key in case_keys if key.strip()]

模式 C:搜索批量操作

# 使用 AskUserQuestion 询问搜索条件
# 可选搜索条件:
# - key_or_name: case key 或名称关键词
# - workspaces: 工作空间列表
# - case_labels: 标签列表
# - runtime_uuid: 运行时 UUID

# 调用 testany_list_cases 获取匹配的 case 列表
cases = testany_list_cases(
    key_or_name=search_keyword,
    workspaces=selected_workspaces,
    case_labels=selected_labels,
    page_size=100  # 限制单页结果数量
)

case_keys = [case['key'] for case in cases]

搜索参数说明:

  • key_or_name: 模糊匹配 case key 或名称
  • workspaces: 筛选指定工作空间的 case(需要先调用 testany_get_my_workspaces 获取可用工作空间列表)
  • case_labels: 筛选包含指定标签的 case(需要先调用 testany_list_labels 获取可用标签列表)
  • page_size: 单页返回数量,默认 20,批量操作建议设为 100

步骤 3:对每个 Case 执行同步流程

遍历 case_keys,对每个 case 执行以下子流程:

子流程 3.1:验证 Case 存在 + 保护已有 secrets

case = testany_get_case(key=case_key)
if case is None or case.get('error'):
    记录错误:Case 不存在
    跳过此 case
    continue

# 读取并隔离已有的 type=secrets 行
# 这类行是用户在 case 上直接维护的凭证绑定,不从源代码派生;
# sync 流程重建 environment_variables 时必须原样保留它们,避免静默丢失绑定。
existing_env_vars = (case.get('case_meta') or {}).get('environment_variables') or []
preserved_secrets = [row for row in existing_env_vars if row.get('type') == 'secrets']
preserved_secret_names = {row['name'] for row in preserved_secrets}

子流程 3.2:解析源代码 URL

case['script_url'] 中提取:

  • 仓库名称(如:testany-io/integration-test)
  • 分支名称(如:demo1、from-swagger-backend-all)
  • 文件路径(如:testany-scripts/xxx/yyy/xxx.py)

URL 格式:

https://bitbucket.org/{owner}/{repo}/src/{branch}/{file_path}

显示解析结果供用户确认:

  • 分支名称
  • 文件路径

子流程 3.3:拉取源代码到本地

使用 Git Worktree(默认方案):

# 统一工作树目录
WORKTREE_BASE="/tmp/testany-worktrees"

# 首次访问某个分支时,创建工作树(如果不存在)
BRANCH_WORKTREE="${WORKTREE_BASE}/{branch_name}"
if [ ! -d "${BRANCH_WORKTREE}" ]; then
    git fetch origin {branch_name}
    git worktree add "${BRANCH_WORKTREE}" origin/{branch_name}
fi

# 从工作树中读取文件
FILE_PATH="${BRANCH_WORKTREE}/{file_path}"

优势:

  • 并行访问多个分支:每个分支有独立工作树
  • 缓存复用:同一分支的多个 case 只需创建一次
  • 无副作用:不影响主工作区的 Git 配置

子流程 3.4:解析入参(Input Parameters)

从源代码的 BASE_STRUCTURES_INFO["structure"] 部分查找所有 ENV[变量名] 格式的占位符:

import re

env_pattern = r'ENV\[([^\]]+)\]'
input_params = set()

for section in ['path', 'query', 'header', 'body']:
    if section in structure:
        text = json.dumps(structure[section])
        matches = re.findall(env_pattern, text)
        input_params.update(matches)

# 跳过与已有 secret 绑定同名的变量:
# 这些名字已经由用户配置的 secret 提供,代码中直接读同名环境变量即可,
# sync 不应该再派生一条 type=env 把它覆盖掉。
input_collisions = input_params & preserved_secret_names
if input_collisions:
    记录警告:f"以下变量与已有 secret 绑定同名,保留 secret,跳过 source 派生:{sorted(input_collisions)}"
    input_params -= input_collisions

入参规范:

  • 变量名保持 ENV[...] 中的原始名称(全大写)
  • 类型:env(表示输入参数)
  • value:使用占位符值 "PLACEHOLDER"
  • description:根据参数位置和用途自动生成

子流程 3.5:解析出参(Output Parameters)

从源代码的 relay_request_fields 数组中提取所有字段:

relay_request_fields = [
    "NAME",
    "DESCRIPTION",
    "RUNTIME_UUID",
    # ...
]

出参规范:

  • 字段名保持数组中的原始名称(全大写)
  • 类型:output(表示 relay 输出参数)
  • value:使用占位符值 "PLACEHOLDER"
  • description:根据字段名称自动生成

与已有 secrets 的冲突处理: 如果 relay_request_fields 中某个字段与已有 secret 绑定同名,保留 secret 绑定,跳过该字段的 output 生成,并记录警告:output 字段 {NAME} 与已有 secret 绑定同名,跳过 output 派生。 (同一 case 内 environment_variables 的 name 必须唯一,不能既是 secrets 又是 output。)

子流程 3.6:读取环境变量文件并替换 PLACEHOLDER

从同一分支的 env 目录中读取环境变量配置文件,并将匹配的入参 value"PLACEHOLDER" 替换为实际值。

环境变量文件路径规则:

元素 示例
Python 文件路径 testany-scripts/get_case_importHistory_id_payload/positive/test_get_case_importHistory_id_payload_positive.py
提取的目录名 get_case_importHistory_id_payloadtestany-scripts/ 后的第一个目录)
环境变量文件路径 integration-test/testany-scripts/env/{目录名}_environment.txt
完整路径 integration-test/testany-scripts/env/get_case_importHistory_id_payload_environment.txt

实现逻辑:

# 步骤 1:从 Python 文件路径中提取目录名
import re
from pathlib import Path

python_path = "testany-scripts/get_case_importHistory_id_payload/positive/xxx.py"

# 提取 testany-scripts/ 后的第一个目录名
match = re.match(r'testany-scripts/([^/]+)/', python_path)
if match:
    directory_name = match.group(1)  # get_case_importHistory_id_payload
else:
    directory_name = None

# 步骤 2:构建环境变量文件路径
if directory_name:
    env_file_path = f"integration-test/testany-scripts/env/{directory_name}_environment.txt"
    # 在 worktree 中的完整路径
    full_env_path = f"{BRANCH_WORKTREE}/{env_file_path}"
else:
    full_env_path = None

# 步骤 3:读取并解析环境变量文件
env_values = {}
if full_env_path and Path(full_env_path).exists():
    with open(full_env_path, 'r') as f:
        for line in f:
            line = line.strip()
            if line and '=' in line and not line.startswith('#'):
                key, value = line.split('=', 1)
                env_values[key.strip()] = value.strip()

# 步骤 4:替换匹配的入参的 value
for param in input_params:
    if param in env_values:
        # 将 PLACEHOLDER 替换为实际值
        param_value = env_values[param]
        environment_variables.append({
            "type": "env",
            "name": param,
            "value": param_value,
            "description": f"Input parameter: {param}"
        })
    else:
        # 文件中未找到该参数,保留 PLACEHOLDER
        environment_variables.append({
            "type": "env",
            "name": param,
            "value": "PLACEHOLDER",
            "description": f"Input parameter: {param}"
        })

环境变量文件格式:

# 示例:get_case_importHistory_id_payload_environment.txt
ENV_QUERY_KEY_1_00=IXX
ENV_PATH_ID_1_00=12345
ENV_HEADER_AUTH_TOKEN=abc123def456

注意:

  • 文件格式为 KEY=VALUE,每行一个变量
  • 支持 # 开头的注释行(忽略)
  • 如果文件不存在,所有入参的 value 保留 "PLACEHOLDER"
  • 如果某个参数在文件中未找到,该参数的 value 保留 "PLACEHOLDER"

子流程 3.7:预览更新

显示将要更新的 environment_variables 数组:

入参(type=env):
- ENV_PATH_ID_1_00: Path parameter for import history id

出参(type=output):
- NAME: Output field from response: NAME
- DESCRIPTION: Output field from response: DESCRIPTION
...

如果 preserved_secrets 非空,额外展示保护提示(在入参/出参列表之前):

⚠️ 检测到 N 个 type=secrets 行,将原样保留(本次 sync 不会修改):
- DB_PASSWORD
- API_TOKEN
...

如果 input_collisions 或 output 冲突非空,同样展示跳过提示:

⚠️ 以下 source 变量与已有 secret 绑定同名,已跳过派生:
- DB_PASSWORD (source 中作为 ENV[...] 出现,保留 secret 绑定)

使用 AskUserQuestion 确认是否执行更新:

  • 选项 1:确认更新
  • 选项 2:跳过此 case
  • 选项 3:跳过剩余所有 case

子流程 3.8:执行更新

# 注意:environment_variables 已经在子流程 3.6 中填充了入参
# 这里只需要添加出参(出参不替换 PLACEHOLDER)

# 添加出参
for field in relay_request_fields:
    environment_variables.append({
        "type": "output",
        "name": field,
        "value": "PLACEHOLDER",
        "description": f"Output field from response: {field}"
    })

# 合并:从 source 派生的 env/output 行 + 子流程 3.1 中保留的 type=secrets 行
# 必须带上 preserved_secrets,否则整集合替换语义会把已有 secret 绑定删除
final_env_vars = environment_variables + preserved_secrets

# 更新 case
result = testany_update_case(
    key=case_key,
    case_meta={
        "environment_variables": final_env_vars
    }
)

记录更新结果(成功/失败/错误信息)。


步骤 4:显示汇总报告

所有 case 处理完成后,显示汇总报告:

同步完成汇总:
----------------
总处理数:10
成功:8
失败:2

失败的 Case:
- ABC12345: Case 不存在
- DEF67890: 源代码拉取失败(分支不存在)

是否清理 worktree 缓存?(缓存可复用,建议保留)

提供清理选项:

  • 选项 1:清理所有 worktree 缓存
  • 选项 2:保留缓存(下次运行更快)

关键代码模式识别

入参模式

代码特征 含义
BASE_STRUCTURES_INFO 请求结构模板
structure.path 路径参数
structure.query 查询参数
structure.header 请求头参数
structure.body 请求体参数
ENV[变量名] 需要解析的环境变量(入参)

出参模式

代码特征 含义
relay_request_fields 出参字段列表(全大写)
response_fields 响应字段定义(用于 process_response_data)
share_test_data(processed_data) 调用 relay 服务传递出参

错误处理

1. Case 不存在

  • 检查 testany_get_case 的返回值
  • 如果 404 错误,记录并跳过

2. 源代码 URL 解析失败

  • 验证 script_url 格式是否符合预期
  • 如果格式不匹配,记录错误并跳过

3. 分支不存在

  • 执行 git fetch 时检查返回值
  • 如果分支不存在,记录错误并跳过

4. 文件拉取失败

  • 检查文件路径是否正确
  • 如果拉取失败,记录错误并跳过

5. 代码模式不匹配

  • 检查 BASE_STRUCTURES_INFOrelay_request_fields 是否存在
  • 如果不存在,提示该 case 可能不是 OpenAPI 生成,记录并跳过

6. 环境变量更新失败

  • 检查 testany_update_case 的错误信息
  • 如果错误码为 E400002case_secrets_feature_disabled),提示用户:该 workspace 的 secrets 功能可能未开启,请联系管理员确认
  • 记录错误详情,继续处理下一个 case

7. 名称冲突(source 变量与已有 secret 绑定同名)

  • 保留 secret 绑定,跳过 source 派生的同名 env/output
  • 在预览中展示跳过列表(子流程 3.7)
  • 不算错误,不中断流程;仅在汇总报告里计入警告

使用示例

示例 1:单 Case 同步

/testany-sync-case-env-from-source
→ 选择操作模式:单 Case 操作
→ 输入 case key: B9121897
→ 执行同步流程...

示例 2:批量 Keys 同步

/testany-sync-case-env-from-source
→ 选择操作模式:批量 Keys 操作
→ 输入 case keys(逗号分隔): B9121897,5C5FF07F,ABC12345
→ 执行同步流程...

示例 3:搜索批量操作

/testany-sync-case-env-from-source
→ 选择操作模式:搜索批量操作
→ 选择搜索条件:按标签搜索
→ 选择标签: swagger-backend
→ 找到 15 个匹配的 case
→ 执行同步流程...

示例 4:搜索条件组合

/testany-sync-case-env-from-source
→ 选择操作模式:搜索批量操作
→ 搜索关键词: importHistory
→ 选择工作空间: demo1
→ 选择标签: swagger-backend
→ 找到 8 个匹配的 case
→ 执行同步流程...

注意事项

1. 代码拉取方式

  • 默认使用 Git Worktree:在 /tmp/testany-worktrees/{branch_name} 创建工作树
  • Worktree 会缓存复用,同一分支的多个 case 只需创建一次
  • 处理完成后可选择保留或清理缓存

2. 批量处理性能

  • 不同分支的 case 可以并行处理(每个分支有独立 worktree)
  • 同一分支的多个 case 可以快速复用已创建的 worktree
  • 建议首次运行前用单 case 测试,确认配置正确

3. 搜索结果限制

  • testany_list_cases 默认返回 20 条结果
  • 批量操作建议设置 page_size=100 或更高
  • 如需处理更多结果,需要分页处理

4. 变量命名

  • 入参:保持 ENV[...] 中的原始名称
  • 出参:保持 relay_request_fields 中的原始名称
  • 所有变量名全大写

5. 值占位符与实际值

  • 入参:从 integration-test/testany-scripts/env/{目录名}_environment.txt 文件中读取实际值替换 PLACEHOLDER
    • 如果文件存在且包含该参数,使用文件中的值(如 ENV_QUERY_KEY_1_00=IXX
    • 如果文件不存在或参数未在文件中找到,保留 "PLACEHOLDER"
  • 出参:始终保持 "PLACEHOLDER"(出参不需要从文件读取)
  • description 字段提供有意义的说明

6. 环境变量文件

  • 文件格式:KEY=VALUE,每行一个变量
  • 支持 # 开头的注释行(自动忽略)
  • 文件路径:integration-test/testany-scripts/env/{目录名}_environment.txt
  • 目录名提取规则:从 testany-scripts/ 后的第一个目录名获取
  • 文件在同一分支的 worktree 中读取(需要先拉取代码到本地)

7. Git 配置

  • 确保用户有正确的 Git 认证配置
  • 私有仓库需要 SSH 或 HTTPS 认证
  • 确保 origin remote 指向正确的仓库

成功标准

  • 成功拉取源代码到本地
  • 正确识别所有 ENV[...] 入参
  • 正确识别所有 relay_request_fields 出参
  • 从 env 目录成功读取环境变量文件并替换入参的 PLACEHOLDER
  • 成功更新 case 的 environment_variables
  • 提供清晰的进度反馈和结果报告
  • 支持 3 种操作模式(单 case、批量 keys、搜索批量)
  • 提供汇总报告和清理选项

参考代码结构

OpenAPI 生成的典型测试代码结构:

BASE_STRUCTURES_INFO = {
    "structure": {
        "path": {
            "id": "ENV[ENV_PATH_ID_1_00]"
        },
        "query": {},
        "header": {},
        "body": None
    }
}

# ... 其他代码 ...

relay_request_fields = [
    "NAME",
    "DESCRIPTION",
    "RUNTIME_UUID",
    "SCRIPT_ADDRESS",
    "CASE_META",
    "CASE_LABELS",
    "CASE_VERSION",
    "IS_PRIVATE",
    "WORKSPACE_KEYS",
    "CREDENTIALS",
]

response_fields = [
    # ... 响应字段定义 ...
]

实现检查清单

  • 实现操作模式选择(单 case / 批量 keys / 搜索批量)
  • 验证 case 存在
  • 解析 script_url 获取仓库、分支、文件路径
  • 检查并创建 Git Worktree(如不存在)
  • 从 worktree 读取源代码文件
  • 解析入参(ENV[...] 模式)
  • 解析出参(relay_request_fields)
  • 从 Python 文件路径提取目录名并构建环境变量文件路径
  • 从 worktree 读取环境变量文件(KEY=VALUE 格式)
  • 将匹配的入参 value 从 PLACEHOLDER 替换为实际值
  • 构建环境变量数组
  • 预览更新结果(用户确认)
  • 调用 testany_update_case
  • 支持批量处理和多分支
  • 生成汇总报告
  • 提供清理 worktree 缓存选项

Git Worktree 清理命令

手动清理所有 worktree 缓存:

# 列出所有 worktree
git worktree list

# 删除特定 worktree
git worktree remove /tmp/testany-worktrees/{branch_name}

# 删除所有 worktree(谨慎使用)
for worktree in /tmp/testany-worktrees/*/; do
    git worktree remove "$worktree"
done