ahel is live on Product Hunt today. Upvote

Testany Case 环境变量同步 Skill

SkillSecurity

Automatically identifies input and output parameters from Testany case source code generated from OpenAPI, and syncs them to the case's environment variable list. Suitable for cases where the source code is stored in a Git repository that requires authentication (e.g., Bitbucket).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Testany Case 环境变量同步 Skill skill

What this skill tells your AI

The instructions your AI receives, as published by testany-io/testany-agent-skills in plugins/testany-bot/skills/SKILL.md and read by ahel’s review.

本 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 keys、搜索条件和操作模式;已明确时直接复用,不重复询问。配置更新的预览与确认门禁保持不变:

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

仅当模式或目标仍不明确时提问;使用当前可用的提问工具,无工具时使用普通文本。


步骤 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)
  • 不算错误,不中断流程;仅在汇总报告里计入警告

使用示例与字段约定

单 case、批量及环境文件示例见 使用参考;处理环境变量前读取其中的密钥、PLACEHOLDER 和字段约定。

成功标准

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

参考代码结构也见 使用参考

实现检查清单

  • 实现操作模式选择(单 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

Signals

GitHub stars
82
Forks
23
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
testany-sync-case-env-from-source
Source
github.com/testany-io/testany-agent-skills