diff --git a/skills/g3fo-commit-jira/.gitignore b/skills/g3fo-commit-jira/.gitignore new file mode 100644 index 0000000..3612d33 --- /dev/null +++ b/skills/g3fo-commit-jira/.gitignore @@ -0,0 +1,2 @@ +# 勿提交含 API Token 的配置文件 +jira_upload.env diff --git a/skills/g3fo-commit-jira/SKILL.md b/skills/g3fo-commit-jira/SKILL.md new file mode 100644 index 0000000..36d6fb2 --- /dev/null +++ b/skills/g3fo-commit-jira/SKILL.md @@ -0,0 +1,164 @@ +--- +name: g3fo-commit-jira +description: G3FO 项目 Git 提交规范自动化工具。强制要求提供 git 修订号(hash),支持用户直接提供 Jira 编号或自动查找/创建 Jira 任务。按任务类型(漏洞修复/新功能/重构等)判定需附加的文档(开发计划、影响分析、任务摘要),生成对应 MD 并按公司规范命名(_Dev_Plan.md / _Impact_Analysis.md / _Task_Summary.md)上传到 Jira 附件,将影响分析写入评论,最后将 Jira 编号追加到 commit message 末尾后提交(不 push)。适用于 G3FO/G3SF 项目的所有 git 提交场景。 +--- + +# G3FO Commit Jira 技能 + +本技能通过 git 修订号自动分析改动,绑定/创建 Jira 任务,按**公司 Jira 工单附件文档政策**生成并上传所需文档(开发计划、影响分析、任务摘要),并更新 commit message。 + +> **公司文档政策**(自 2026-03-09 起):何时附加哪些文件见 `references/jira_commit_docs_policy.md`。 +> 报告模板、任务类型判定与 JQL 参考 `references/workflow.md`。 + +--- + +## 前置要求 + +**1. 强制提供 Git 修订号 (hash)** + +用户必须提供一个有效的 git 修订号(如 `HEAD` 或具体的 `hash`)。 +- **校验逻辑**:使用 `git rev-parse ` 检查修订号是否存在。 +- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:“找不到修订号 ``,请提供正确的 git 修订号(例如 HEAD 或 7 位以上的 commit hash)。” +- **路径要求**:必须在相关的项目根目录下执行 git 命令。如果当前目录不是 git 仓库或不是目标项目,请提示用户切换到正确的项目路径。 + +**2. Jira 编号 (可选)** + +用户可以主动提供 Jira 编号(如 `G3SF-123`)。 +- 如果提供了 Jira 编号,直接引用该编号,跳过“查找/选择 Jira 任务”的步骤。 +- 如果未提供,则按流程自动查找或提示用户新建。 + +**3. Atlassian MCP 授权** + +- 查看 `mcps/plugin-atlassian-atlassian/STATUS.md` 确认已在 Cursor 中授权 MCP 插件。 +- 如果未授权,调用 `mcp_auth`,server: `plugin-atlassian-atlassian`,参数 `{}`。 +- 若 MCP 插件未安装,引导用户按以下步骤操作: + 1. 打开 Cursor Settings -> MCP + 2. 添加名为 `Atlassian` 的插件 + 3. 按照提示完成授权并启用 skill +- 确保可用工具在 `mcps/plugin-atlassian-atlassian/tools/` 目录下已列出。 + +**4. 上传附件配置(用于将影响分析报告 MD 上传到 Jira)** + +- Atlassian MCP 插件不支持上传文件,本技能使用自带脚本 `scripts/upload_attachment.py` 将报告 MD 上传为 Issue 附件。 +- 在技能目录或脚本目录放置 `jira_upload.env`(或使用 `--config` 指定路径),包含:`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`。格式见 `jira_upload.env.example`,详细说明见 `references/env_config.md`。 +- 若未配置,上传附件步骤跳过,仅通过 `jira_add_comment` 写入报告;已配置则先上传 MD 再写评论。 + +--- + +## 执行步骤 + +### Step 1 - 获取提交内容 + +```powershell +git show # 获取 diff + 元数据 +git log -1 --format="%B" # 获取原始 commit message +``` + +分析要点: +- 涉及的服务模块/包名 +- 新增/修改/删除的方法与接口 +- 逻辑改动的核心目的 + +### Step 2 - 判定任务类型并确定需生成的文档 + +根据改动内容与 `references/jira_commit_docs_policy.md` 中的**详细条件表**,先判定任务类型,再决定需要生成并上传的文档: + +| 任务类型 | 开发计划 | 影响分析 | 任务摘要 | +|----------|:--------:|:--------:|:--------:| +| 漏洞修复——简单(如拼写、界面对齐) | ❌ | ❌ | ❌ | +| 漏洞修复——中等(范围有限的逻辑错误) | ❌ | ✅ | ❌ | +| 漏洞修复——复杂/高风险(并发、核心模块) | ✅ | ✅ | ✅ | +| 小幅度增强(如小效用方法) | ❌ | ✅ | ❌ | +| 新功能/模块 | ✅ | ✅ | ✅ | +| 主要重构 | ✅ | ✅ | ✅ | +| 配置/基础设施变更 | ❌ | ✅ | ❌ | + +- **无需任何文档**时:仅执行 Step 3(确定 Jira)、Step 5(amend),不生成报告、不写评论、不上传附件;仍将 Jira 编号追加到 commit message。 +- **需要文档**时:继续 Step 2 下半部分,生成对应内容。 + +### Step 2(续)- 生成所需报告内容 + +对上述判定为“需要”的文档,按 `references/workflow.md` 中的模板生成内容(可先存于内存或临时文件): + +- **开发计划**:步骤、设计方法、技术决策。 +- **影响分析**:改动概览、方法级分析、影响范围、风险与回滚、验证与测试(模板见 workflow.md)。 +- **任务摘要**:所完成工作的简要回顾,以及任何偏离计划之处。 + +生成后先不按“公司附件名”落盘,等取得 Jira 编号后再以 `_*.md` 命名保存并上传。 + +### Step 3 - 确定 Jira 任务 + +**1. 如果用户已提供 Jira 编号:** +- 直接使用该编号(如 `G3SF-123`),进入 Step 4。 + +**2. 如果用户未提供 Jira 编号:** +- 使用 JQL 查找当前用户在 G3SF 项目下的进行中任务(limit 5): +``` +project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC +``` +- 展示列表供用户选择,或允许用户输入 `0` 新建。 +- 如果搜索结果为空,自动进入新建流程。 + +### Step 4 - 写入 Jira(附件 + 评论) + +**附件命名规范(公司要求)**:上传到 Jira 的 MD 文件名必须为 +`_Dev_Plan.md`、`_Impact_Analysis.md`、`_Task_Summary.md`。 +根据 Step 2 判定结果,仅上传“需要”的文档;每个文件先按该命名写入本地再上传(脚本以本地文件名为 Jira 附件名)。 + +**如果选择/新建的任务 ID > 0:** + +1. **保存并上传附件**(已配置 `jira_upload.env` 且本任务需要文档时): + 将 Step 2 生成的各文档内容,按公司规范命名写入当前项目目录(或 `docs/` 等),再调用上传脚本。PowerShell 中不要用 `&&`,改用 `;` 或换行。 + + ```powershell + # 示例:需要三份文档时,先写入 G3SF-123_Dev_Plan.md、G3SF-123_Impact_Analysis.md、G3SF-123_Task_Summary.md,再: + python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-123" --file "G3SF-123_Impact_Analysis.md" --file "G3SF-123_Dev_Plan.md" --file "G3SF-123_Task_Summary.md" + ``` + + 若本任务仅需影响分析,则只写入并上传 `G3SF-123_Impact_Analysis.md`。未配置 `jira_upload.env` 时跳过上传,仅写评论。 +2. **写评论**:若有影响分析内容,调用 `jira_add_comment` 将影响分析报告正文作为评论写入 issue。 +3. 记录该任务的 issue key。 + +**如果用户选择 0 或搜索无结果:** + +- 调用 `jira_create_issue` 创建新任务,详情见 `references/workflow.md` +- 设置 project key: `G3SF`, issue type: `Task` +- **Summary**:必须在标题前添加 `[G3SF]` 前缀,格式为 `[G3SF] 提炼后的改动摘要` +- **Description**:完整影响分析报告(Markdown 格式);若本任务需开发计划/任务摘要,可在创建后通过附件上传 +- **Assignee**:必须设置为当前登录账号(由 MCP 传入当前用户或等效参数) +- 获取返回的新 issue key(如 `G3SF-123`) +- 若已配置 `jira_upload.env` 且本任务需要文档,创建成功后按公司命名写入并上传对应 MD 文件到该新 Issue + +### Step 5 - 更新 Git 提交信息 + +使用获取到的 Jira 编号,通过 `git commit --amend` 更新 commit message。**严禁 push**。 + +**注意**:在 Windows PowerShell 环境下,需确保 UTF8 编码以防止中文乱码。 + +```powershell +# 设置 UTF8 编码 +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$origMsg = (git log -1 --format="%B").TrimEnd() +$jiraKey = "G3SF-123" # 实际获取到的 key + +# 使用 --cleanup=verbatim 确保原始消息格式不被 git 自动修剪导致编码转换问题 +git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim +``` + +完成后告知用户: + +``` +绑定成功: + Jira: G3SF-123 (https://your-jira/browse/G3SF-123) + Commit: +``` + +--- + +## 相关参考 + +- **公司 Jira 附件文档政策**(何时附加哪些文件):`references/jira_commit_docs_policy.md` +- **报告模板、三文档说明与错误处理**:`references/workflow.md` +- **上传附件配置**:`references/env_config.md`;脚本 `scripts/upload_attachment.py`,依赖见 `scripts/requirements.txt` diff --git a/skills/g3fo-commit-jira/jira_upload.env.example b/skills/g3fo-commit-jira/jira_upload.env.example new file mode 100644 index 0000000..a4f1158 --- /dev/null +++ b/skills/g3fo-commit-jira/jira_upload.env.example @@ -0,0 +1,6 @@ +# Jira 上传附件配置示例 +# 复制为 jira_upload.env 并填入真实值,勿提交 jira_upload.env 到 Git + +JIRA_BASE_URL=https://你的站点.atlassian.net +JIRA_EMAIL=你的邮箱@company.com +JIRA_API_TOKEN=你的 API Token diff --git a/skills/g3fo-commit-jira/references/env_config.md b/skills/g3fo-commit-jira/references/env_config.md new file mode 100644 index 0000000..a4c7e54 --- /dev/null +++ b/skills/g3fo-commit-jira/references/env_config.md @@ -0,0 +1,71 @@ +# Jira 上传附件 — 配置文件说明 + +本技能通过脚本 `scripts/upload_attachment.py` 将影响分析报告 MD 文件上传到 Jira Issue 附件(Atlassian MCP 插件不支持上传文件)。脚本从**配置文件**或环境变量读取 Jira 认证与站点信息,环境变量可覆盖配置文件中的同名项。 + +--- + +## 配置文件方式(推荐) + +### 1. 创建配置文件 + +在技能目录 `skills/g3fo-commit-jira/` 下放置 `jira_upload.env`,或复制示例后改名并填写真实值: + +```powershell +# 在技能目录下 +copy jira_upload.env.example jira_upload.env +# 编辑 jira_upload.env,填入 JIRA_BASE_URL、JIRA_EMAIL、JIRA_API_TOKEN +``` + +### 2. 文件格式 + +`jira_upload.env` 为 KEY=VALUE 格式,支持 `#` 注释: + +```ini +# Jira 站点 URL,不要带末尾 / +JIRA_BASE_URL=https://你的站点.atlassian.net +# 登录邮箱 +JIRA_EMAIL=你的邮箱@company.com +# API Token(在 Atlassian 账户设置中创建) +JIRA_API_TOKEN=你的 API Token +``` + +### 3. 指定配置文件路径 + +从仓库根目录执行时,建议用 `--config` 指定技能目录下的配置文件: + +```powershell +python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue G3SF-123 --file "G3SF-123_Impact_Analysis.md" +``` + +--- + +## 配置项说明 + +| 配置项 | 说明 | 示例 | +|--------|------|------| +| `JIRA_BASE_URL` | Jira 站点 URL,不要带末尾 `/` | `https://your-domain.atlassian.net` | +| `JIRA_EMAIL` | 登录 Jira 的邮箱 | `you@company.com` | +| `JIRA_API_TOKEN` | Jira 账户的 API Token | 在 [Atlassian API Tokens](https://id.atlassian.com/manage-profile/security/api-tokens) 创建 | + +--- + +## 环境变量覆盖 + +若同时存在配置文件和环境变量,**环境变量优先**。 + +--- + +## API Token 获取 + +1. 打开 https://id.atlassian.com/manage-profile/security/api-tokens +2. 使用 Jira 登录邮箱登录 +3. 点击 “Create API token”,命名后复制 +4. Token 只显示一次,请妥善保存 + +--- + +## 安全注意 + +- **不要将 `jira_upload.env` 提交到 Git**。技能目录下已通过 `.gitignore` 忽略。 +- 不要将 `JIRA_API_TOKEN` 写入代码或提交到仓库。 +- 定期在 Atlassian 账户中轮换 API Token。 diff --git a/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md b/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md new file mode 100644 index 0000000..ab06860 --- /dev/null +++ b/skills/g3fo-commit-jira/references/jira_commit_docs_policy.md @@ -0,0 +1,48 @@ +# Jira 工单附件文档政策(公司强制性指南) + +自 **2026年3月9日** 起,以下为将 **开发计划**、**影响分析**、**任务摘要** 附加到 Jira 工单的强制性指南,用于跟踪设计决策、评估风险、为未来参考提供背景及项目间协作。 + +--- + +## 一般规则(经验法则) + +| 场景 | 需要附加的文档 | +|------|----------------| +| **漏洞修复** | 仅包括 **影响分析**(若修复有非简单副作用) | +| **新功能 / 重大核心变更** | **全部三个文件**:开发计划、影响分析、任务摘要 | +| **琐碎 / 小改动** | 无需文件 | + +--- + +## 详细条件(按任务类型) + +| 任务类型 | 开发计划 | 影响分析 | 任务摘要 | +|----------|:--------:|:--------:|:--------:| +| 漏洞修复——简单(如拼写错误、界面对齐) | ❌ | ❌ | ❌ | +| 漏洞修复——中等(如范围有限的逻辑错误) | ❌ | ✅ | ❌ | +| 漏洞修复——复杂/高风险(如并发、核心模块) | ✅ | ✅ | ✅ | +| 小幅度增强(如添加小效用方法) | ❌ | ✅ | ❌ | +| 新功能/模块 | ✅ | ✅ | ✅ | +| 主要重构 | ✅ | ✅ | ✅ | +| 配置/基础设施变更 | ❌ | ✅ | ❌ | + +--- + +## 文档含义说明 + +- **开发计划**:概述步骤、设计方法和技术决策。 +- **影响分析**:列出受影响区域、风险、性能考虑因素及向后兼容性。 +- **任务摘要**:简要回顾所完成的工作,包括任何偏离计划的地方。 + +--- + +## 提交方式与命名规范 + +- 将文件(Markdown)**直接放在工单的附件部分**。 +- 使用一致的命名规范: + - `_Dev_Plan.md` + - `_Impact_Analysis.md` + - `_Task_Summary.md` + +示例:工单 `G3SF-123` 的三个附件命名为 +`G3SF-123_Dev_Plan.md`、`G3SF-123_Impact_Analysis.md`、`G3SF-123_Task_Summary.md`。 diff --git a/skills/g3fo-commit-jira/references/workflow.md b/skills/g3fo-commit-jira/references/workflow.md new file mode 100644 index 0000000..6412ac9 --- /dev/null +++ b/skills/g3fo-commit-jira/references/workflow.md @@ -0,0 +1,333 @@ +# G3FO Commit Jira — 详细工作流参考 + +## 目录 +1. [公司 Jira 附件文档政策](#公司-jira-附件文档政策) +2. [Step 1: 获取并解析提交内容](#step-1-获取并解析提交内容) +3. [Step 2: 任务类型判定与三文档模板](#step-2-任务类型判定与三文档模板) +4. [Step 3: Jira 任务查找与确认](#step-3-jira-任务查找与确认) +5. [Step 4: Jira 字段映射与写入](#step-4-jira-字段映射与写入) +6. [Step 5: Git Amend 命令详解](#step-5-git-amend-命令详解) +7. [错误处理表](#错误处理表) +8. [Atlassian MCP 工具速查](#atlassian-mcp-工具速查) + +--- + +## 公司 Jira 附件文档政策 + +自 **2026年3月9日** 起,何时附加「开发计划」「影响分析」「任务摘要」以公司强制性指南为准。 +完整政策与详细条件表见 **`references/jira_commit_docs_policy.md`**。 + +- **附件命名规范**:上传到 Jira 的 MD 必须命名为 + `_Dev_Plan.md`、`_Impact_Analysis.md`、`_Task_Summary.md` + 例如:`G3SF-123_Impact_Analysis.md`。 +- 根据任务类型仅生成并上传“需要”的文档;无需文档的琐碎改动仍可绑定 Jira 并 amend,但不生成报告、不写评论、不上传附件。 + +--- + +## Step 1: 获取并解析提交内容 + +### 命令 + +```powershell +# 检查 hash 是否为 HEAD +$head = git rev-parse HEAD +$provided = git rev-parse +if ($head -ne $provided) { Write-Error "提交不是 HEAD,无法 amend" } + +# 获取完整 diff 和元数据 +git show + +# 仅获取原始 commit message(用于后续 amend 保留) +git log -1 --format="%B" + +# 列出修改的文件(不含 diff 内容,快速预览) +git diff-tree --no-commit-id -r --name-status +``` + +### 解析要点 + +从 `git show` 输出中提取: + +| 信息 | 说明 | +|------|------| +| `--- a/path` / `+++ b/path` | 修改的文件路径 | +| `@@ ... @@` 上下文 | 修改的类名、方法名 | +| `+` 行 | 新增代码(方法签名、接口、配置) | +| `-` 行 | 删除/替换代码 | +| 文件路径中的包名 | 推断所属服务(`g3fo-trade-service`, `g3fo-margin-service` 等) | + +G3FO 服务包路径规律: +- `com.afe.g3fo..*` → 对应服务 +- `controller` / `api` → REST 接口层 +- `facade` / `facade.impl` → Dubbo 接口层 +- `service` / `service.impl` → 业务逻辑层 + +--- + +## Step 2: 任务类型判定与三文档模板 + +### 2.1 按任务类型确定需生成的文档 + +| 任务类型 | 开发计划 | 影响分析 | 任务摘要 | +|----------|:--------:|:--------:|:--------:| +| 漏洞修复——简单(如拼写、界面对齐) | ❌ | ❌ | ❌ | +| 漏洞修复——中等(范围有限的逻辑错误) | ❌ | ✅ | ❌ | +| 漏洞修复——复杂/高风险(并发、核心模块) | ✅ | ✅ | ✅ | +| 小幅度增强(如小效用方法) | ❌ | ✅ | ❌ | +| 新功能/模块 | ✅ | ✅ | ✅ | +| 主要重构 | ✅ | ✅ | ✅ | +| 配置/基础设施变更 | ❌ | ✅ | ❌ | + +生成内容后,在**取得 Jira 编号后**按公司规范保存为 `_Dev_Plan.md` / `_Impact_Analysis.md` / `_Task_Summary.md` 再上传。无需文档时跳过生成与上传,仅绑定 Jira 并 amend。 + +### 2.2 影响分析报告模板 + +生成影响分析时严格按以下结构输出(Markdown 格式): + +```markdown +## 影响分析报告 + +### 1. 改动概览 +- **背景与目标**:[说明为什么改、解决什么问题] +- **涉及模块**:[列举受影响的服务/模块,如 g3fo-trade-service、g3fo-margin-service] +- **改动类型**:功能新增 / 缺陷修复 / 重构 / 配置变更(选一或多个) + +### 2. 方法级改动分析 + +| 文件/类 | 方法/接口 | 改动说明 | 行为是否变更 | +|---------|----------|---------|------------| +| XxxService.java | `calculateMargin()` | 新增空值校验 | 否(边界补强) | + +### 3. 调用方与影响范围 + +| 调用方 | 文件路径 | 影响说明 | +|--------|---------|---------| +| XxxController | controller/XxxController.java | 入参校验增强,现有调用方兼容 | + +- **破坏性变更**:是 / 否 +- 如为"是",具体变更点:[列出方法签名、返回值、异常类型变化] + +### 4. 风险与回滚 +- **风险级别**:低 / 中 / 高 +- **风险说明**:[说明原因] +- **回滚方式**:回退版本 / 关闭特性开关 / 回滚配置 +- **回滚方式是否简单**:是 / 否 + +### 5. 验证与测试 +- **已执行测试**:单测 / 集成测试 / 冒烟测试(选已执行的) +- **关键用例**:[简述关键场景及预期结果] +- **需线上观察**:[列出需关注的日志关键字、监控指标或告警项] +``` + +> 如果 diff 信息不足以确认调用方,在报告中注明:「建议在代码库中搜索 `<方法名>` 确认所有调用方」。 + +### 2.3 开发计划模板(仅当任务类型要求时生成) + +开发计划用于概述步骤、设计方法和技术决策。结构示例: + +```markdown +## 开发计划 + +### 1. 目标与范围 +- **目标**:[本任务要达成的结果] +- **范围**:[涉及模块、接口、配置等] + +### 2. 步骤与设计方法 +1. [步骤一:如需求分析、接口设计] +2. [步骤二:如实现方案、关键类/方法] +3. [步骤三:如测试与验证] + +### 3. 技术决策 +- [关键技术选型或实现方式及理由] +- [与现有架构的衔接方式] +``` + +### 2.4 任务摘要模板(仅当任务类型要求时生成) + +任务摘要用于简要回顾所完成的工作,包括任何偏离计划的地方。结构示例: + +```markdown +## 任务摘要 + +### 1. 完成内容 +- [已完成的主要改动与交付物] + +### 2. 与计划差异(如有) +- [若实际实现与开发计划有偏差,在此说明原因与结果] + +### 3. 后续建议(可选) +- [遗留事项、后续优化或文档更新建议] +``` + +--- + +## Step 3: Jira 任务查找与确认 + +### JQL 查询 + +``` +project = G3SF AND assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC +``` + +- 限制返回 5 条,避免信息过载 +- 若返回 0 条,直接告知用户并自动进入新建流程 + +### 展示格式 + +``` +找到以下 G3SF 进行中任务,请输入编号绑定,或输入 0 新建: + +1. G3SF-101 [修复保证金计算逻辑] 状态: In Progress +2. G3SF-98 [交易服务批量撤单性能优化] 状态: To Do +3. G3SF-95 [用户服务登录接口重构] 状态: In Progress + +0. 新建一个 G3SF Task + +请输入 (1/2/3/0): +``` + +### 确认规则 + +| 用户输入 | 行为 | +|---------|------| +| `1`~`N` | 使用对应任务,写入评论 | +| `0` | 新建 Jira Task | +| `n` / `no` | 等同于 `0`,新建任务 | +| 其他 | 重新提问一次 | + +--- + +## Step 4: Jira 字段映射与写入 + +### 新建 Issue 字段 + +调用 `jira_create_issue`(或鉴权后发现的等效工具),传入: + +| 字段 | 值 | +|------|----| +| `project` | `G3SF` | +| `issuetype` | `Task` | +| `summary` | **必须带 `[G3SF]` 前缀**,格式:`[G3SF] 提炼后的一句话描述`(≤100 字符) | +| `description` | 完整影响分析报告(Markdown / ADF 格式) | +| `assignee` | **必须为当前登录账号**(传入当前用户 accountId/name,确保新建任务经办人为自己) | + +Summary 生成规则: +- **标题必须统一加前缀**:`[G3SF]` + 正文。例如:`[G3SF] [g3fo-trade-service] 修复批量撤单时的并发锁问题` +- 正文优先使用原始 commit message 的第一行(若描述清晰),否则根据修改内容生成:`[服务名] 功能描述` + +新建 Issue 成功后,若已配置 `jira_upload.env` 且本任务需要文档,按公司命名将对应 MD 写入本地后上传到该新 Issue。 + +### 保存报告为 MD 并上传附件 + +1. **按公司规范命名并保存**:取得 Jira 编号(如 `G3SF-123`)后,将 Step 2 生成的各文档内容分别写入本地文件,文件名必须为: + - `_Dev_Plan.md` + - `_Impact_Analysis.md` + - `_Task_Summary.md` + 仅保存本任务类型要求的那几份(见 [2.1 按任务类型确定需生成的文档](#21-按任务类型确定需生成的文档))。保存位置:当前 Git 项目根目录或项目约定目录(如 `docs/`)。 +2. **上传到 Jira 附件**:Atlassian MCP 不支持上传文件,使用本技能自带脚本上传上述 MD 到当前 Issue: + - 配置文件:在技能目录下配置 `jira_upload.env`(`JIRA_BASE_URL`、`JIRA_EMAIL`、`JIRA_API_TOKEN`),详见 `references/env_config.md`。 + - 命令示例(PowerShell 用 `;` 连接,勿用 `&&`),按需上传多文件: + ```powershell + python skills/g3fo-commit-jira/scripts/upload_attachment.py --config "skills/g3fo-commit-jira/jira_upload.env" --issue "G3SF-101" --file "G3SF-101_Impact_Analysis.md" --file "G3SF-101_Dev_Plan.md" --file "G3SF-101_Task_Summary.md" + ``` + - 未配置 `jira_upload.env` 时跳过上传,仅写评论。 + +### 写入已有 Issue(评论) + +调用 `jira_add_comment`(或等效工具),字段: + +| 字段 | 值 | +|------|----| +| `issue_key` | 用户选择的 issue key,如 `G3SF-101` | +| `body` | 完整影响分析报告(与已保存的 MD 内容一致) | + +### Jira 描述格式兼容 + +部分 Jira 实例使用 Atlassian Document Format (ADF) 而非纯 Markdown。若 MCP 工具提示格式错误,则: +- 将 Markdown 内容作为纯文本写入 +- 或使用 `{code}` 块包裹(Jira Wiki 格式兼容) + +--- + +## Step 5: Git Amend 命令详解 + +### PowerShell(Windows 环境) + +在 Windows PowerShell 中,直接使用 `$origMsg` 可能会导致编码问题(乱码)。必须显式指定输出编码为 UTF8。 + +```powershell +# 设置控制台和输出编码为 UTF8,防止中文乱码 +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# 获取原始 commit message(保留完整内容) +$origMsg = (git log -1 --format="%B").TrimEnd() + +# Jira key(从上一步获取) +$jiraKey = "G3SF-123" + +# 追加 Jira 行并 amend(不 push) +# 使用 --cleanup=verbatim 确保消息内容不被 git 自动修剪导致编码转换问题 +git commit --amend -m "$origMsg`n`n**** Jira $jiraKey" --cleanup=verbatim +``` + +### Bash/Linux(供参考) + +```bash +origMsg=$(git log -1 --format="%B" | sed 's/[[:space:]]*$//') +jiraKey="G3SF-123" +git commit --amend -m "$origMsg + +**** Jira $jiraKey" +``` + +### Amend 注意事项 + +- `--amend` 只修改 HEAD,不改变工作区内容 +- 如果有 GPG 签名要求,告知用户需确认签名 +- amend 后 commit hash 会变更,这是正常现象 +- **禁止 push**:本 skill 不执行 `git push`,提交操作到 `amend` 为止 + +### 结果验证 + +```powershell +git log -1 --format="%B" # 确认 Jira 行已追加 +git log --oneline -1 # 查看 commit 摘要 +``` + +--- + +## 错误处理表 + +| 错误场景 | 检测方式 | 处理方式 | +|---------|---------|---------| +| 修订号不存在 | `git rev-parse ` 报错 | **停止**,提示用户提供正确的修订号。 | +| 提交 hash 不是 HEAD | `git rev-parse HEAD` ≠ `git rev-parse ` | 停止,提示用户确认 hash(只有 HEAD 才能 amend)。 | +| 项目路径不正确 | `git rev-parse --is-inside-work-tree` 失败 | 提示用户在正确的 git 项目目录下操作。 | +| Atlassian MCP 未鉴权 | `STATUS.md` 有提示 / 工具调用返回 401 | 调用 `mcp_auth`,等待用户完成授权。 | +| Atlassian 插件未安装 | `tools/` 目录为空或 MCP 调用失败 | 引导用户在 Cursor Settings → MCP 安装 Atlassian 插件 | +| Jira 项目 G3SF 不存在 | `jira_create_issue` 返回 404/400 | 提示用户确认 Jira 实例 URL 和项目 key | +| 工作区有未提交变更 | `git status --porcelain` 有输出 | 警告:amend 会将未暂存变更排除在外,建议先 `git add` 或 stash | +| commit message 中已有 Jira 行 | message 中包含 `**** Jira` | 跳过追加,提示用户该提交已绑定 | +| Jira 搜索无结果 | 工具返回空列表 | 直接进入新建流程,无需用户确认 | +| 未配置 jira_upload.env | 上传前检查配置不存在 | 跳过上传附件,仅通过 jira_add_comment 写入报告;提示用户可配置后使用脚本上传 | +| 任务类型判定为「无需文档」 | 按政策表属简单漏洞/琐碎改动 | 不生成报告、不写评论、不上传附件;仍执行 Jira 绑定与 commit --amend,将 Jira 编号追加到 commit message | + +--- + +## Atlassian MCP 工具速查 + +鉴权成功后,通过列举 `mcps/plugin-atlassian-atlassian/tools/` 确认可用工具。常见工具名: + +| 功能 | 可能的工具名 | +|------|------------| +| 搜索 Issue | `jira_search_issues`, `jira_jql_search` | +| 创建 Issue | `jira_create_issue` | +| 获取 Issue 详情 | `jira_get_issue` | +| 添加评论 | `jira_add_comment` | +| 更新 Issue | `jira_update_issue` | +| 获取当前用户 | `jira_get_current_user` | +| 获取项目列表 | `jira_get_projects` | +| 上传附件 | MCP 不支持,使用 `scripts/upload_attachment.py` + `jira_upload.env` | + +> 实际工具名以 `tools/` 目录下的 JSON 文件名为准,鉴权后务必先列目录确认。上传附件需使用本技能自带的 Python 脚本,见 `references/env_config.md`。 diff --git a/skills/g3fo-commit-jira/scripts/requirements.txt b/skills/g3fo-commit-jira/scripts/requirements.txt new file mode 100644 index 0000000..a8608b2 --- /dev/null +++ b/skills/g3fo-commit-jira/scripts/requirements.txt @@ -0,0 +1 @@ +requests>=2.28.0 diff --git a/skills/g3fo-commit-jira/scripts/upload_attachment.py b/skills/g3fo-commit-jira/scripts/upload_attachment.py new file mode 100644 index 0000000..3af2984 --- /dev/null +++ b/skills/g3fo-commit-jira/scripts/upload_attachment.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +Upload one or more files as attachments to a Jira Cloud issue via REST API v3. + +Config: from jira_upload.env (current dir or script dir) or env vars. +Usage: + python upload_attachment.py --issue ISSUE_KEY --file path1 [--file path2 ...] + python upload_attachment.py --config /path/to/jira_upload.env --issue KEY --file path1 +""" + +import argparse +import base64 +import os +import sys +from pathlib import Path +from typing import Dict, Optional + +try: + import requests +except ImportError: + print("ERROR: 'requests' is required. Run: pip install requests", file=sys.stderr) + sys.exit(1) + +CONFIG_FILENAME = "jira_upload.env" + + +def _find_config_file(explicit_path: Optional[str], script_dir: Path) -> Optional[Path]: + if explicit_path: + p = Path(explicit_path) + return p if p.exists() and p.is_file() else None + cwd_file = Path.cwd() / CONFIG_FILENAME + if cwd_file.exists(): + return cwd_file + script_dir_file = script_dir / CONFIG_FILENAME + if script_dir_file.exists(): + return script_dir_file + return None + + +def load_config(config_path: Optional[str]) -> Dict[str, str]: + """Load KEY=VALUE from jira_upload.env. Env vars override file values.""" + script_dir = Path(__file__).resolve().parent + path = _find_config_file(config_path, script_dir) + out = {} + if path: + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + k, _, v = line.partition("=") + out[k.strip()] = v.strip().strip('"').strip("'") + for key in ("JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"): + if key in os.environ: + out[key] = os.environ[key] + return out + + +def main() -> int: + parser = argparse.ArgumentParser(description="Upload files as attachments to a Jira issue") + parser.add_argument("--issue", required=True, help="Jira issue key (e.g. G3SF-123)") + parser.add_argument("--file", action="append", required=True, dest="files", help="Path to file to upload (can be repeated)") + parser.add_argument("--config", default=None, help=f"Path to config file (default: {CONFIG_FILENAME} in cwd or script dir)") + args = parser.parse_args() + + cfg = load_config(args.config) + base_url = (cfg.get("JIRA_BASE_URL") or "").rstrip("/") + email = cfg.get("JIRA_EMAIL") + token = cfg.get("JIRA_API_TOKEN") + + if not base_url or not email or not token: + print("ERROR: Set JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN in config file or environment.", file=sys.stderr) + print(f" Config file: {CONFIG_FILENAME} (in current dir or script dir), or use --config PATH", file=sys.stderr) + return 1 + + issue_key = args.issue.strip() + files_to_upload = [] + for p in args.files: + path = Path(p) + if not path.exists(): + print(f"ERROR: File not found: {path}", file=sys.stderr) + return 1 + if not path.is_file(): + print(f"ERROR: Not a file: {path}", file=sys.stderr) + return 1 + files_to_upload.append(path) + + url = f"{base_url}/rest/api/3/issue/{issue_key}/attachments" + auth_str = base64.b64encode(f"{email}:{token}".encode()).decode() + headers = { + "Authorization": f"Basic {auth_str}", + "X-Atlassian-Token": "no-check", + } + + for path in files_to_upload: + with open(path, "rb") as f: + files = {"file": (path.name, f)} + try: + resp = requests.post(url, headers=headers, files=files, timeout=60) + except requests.RequestException as e: + print(f"ERROR: Request failed for {path.name}: {e}", file=sys.stderr) + return 1 + + if resp.status_code not in (200, 201): + print(f"ERROR: Upload failed for {path.name}: HTTP {resp.status_code}", file=sys.stderr) + if resp.text: + print(resp.text[:500], file=sys.stderr) + return 1 + + data = resp.json() + if isinstance(data, list) and data: + for att in data: + print(f"OK: {att.get('filename', path.name)} (id={att.get('id')}, size={att.get('size', '?')})") + else: + print(f"OK: {path.name}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/g3fo-docs/references/middleware/redis/deploy.md b/skills/g3fo-docs/references/middleware/redis/deploy.md index 1232488..5a53640 100644 --- a/skills/g3fo-docs/references/middleware/redis/deploy.md +++ b/skills/g3fo-docs/references/middleware/redis/deploy.md @@ -230,6 +230,83 @@ sentinel deny-scripts-reconfig yes | `sentinel failover-timeout` | 故障转移超时时间 | `180000` | | `sentinel announce-ip` | 宣告哨兵外部 IP | 宿主机实际 IP | +### 4.3 哨兵数量与 Master 选举 + +#### 如何选出新的 Redis Master(故障转移) + +这里的「Master」指 **Redis 主节点**(可写的那台),不是哨兵本身。流程简述如下: + +1. **判定主节点下线** + 每个哨兵定期探测 Redis 主节点。当某个哨兵认为主节点 **主观下线(SDOWN)** 后,会向其他哨兵询问;当达到 **quorum** 个哨兵都认为主节点不可达时,主节点被判定为 **客观下线(ODOWN)**。 + +2. **选举 Sentinel 领导者** + 哨兵之间通过 Raft 式选举选出一个 **Leader**,由它来执行后续故障转移(只会有唯一一个 Sentinel 在执行 failover)。 + +3. **选出新的 Redis Master** + Leader 在**当前存活的从节点**中选一个提升为新主,选择规则通常为: + - 若配置了 `replica-priority`(哨兵侧为 `sentinel replica-priority`),优先选优先级最高且可达的从节点; + - 否则按**复制偏移量**优先选数据最新的从节点; + - 再按 `runid` 字典序等做 tie-break。 + 选好后向该从节点下发 `REPLICAOF NO ONE`,使其成为新主,其余从节点改为复制新主。 + +所以:**“选出 master 节点” = 由 Sentinel Leader 在从节点里按优先级/复制进度选一个,并执行提升为主。** + +#### 是否一定要三台哨兵以上? + +| 哨兵数量 | quorum 典型值 | 说明 | +|----------|----------------|------| +| **1 台** | 1 | 可以触发故障转移,但哨兵单点故障则无人做 failover,**不推荐生产**。 | +| **2 台** | 1 或 2 | quorum=1:能 failover,但两台意见不一致时可能脑裂;quorum=2:需两台都同意才 failover,任一台宕机则永远达不到 2,**无法触发故障转移**。 | +| **3 台** | **2** | 至少 2 台同意才客观下线并执行 failover;允许 1 台哨兵宕机仍可完成选举与切换,**生产推荐最少 3 台 + quorum=2**。 | +| 5 台 | 3 | 可容忍 2 台哨兵同时宕机。 | + +**结论**: +- **不是“必须大于 3 台”**,但 **1 台无高可用、2 台难以兼顾安全与可用**,因此 **至少 3 台哨兵 + quorum=2** 是常见最小生产配置,这样既能选出新 master,又能在挂掉 1 台哨兵时继续工作。 + +quorum 在配置里即 `sentinel monitor mymaster ` 的最后一个数字,例如: + +```Properties +sentinel monitor mymaster 192.168.3.200 6379 2 +``` + +表示至少 **2 个**哨兵认为主节点下线,才会触发客观下线和后续的 Master 选举与故障转移。 + +#### 两哨兵存活却未选出主节点时的排查 + +当前是**两台 Redis 主机 + 两台哨兵**且两台哨兵都存活时,若一直没选出新主,可按下面逐项查。 + +1. **quorum 是否已达到** + 客观下线(ODOWN)需要至少 quorum 个哨兵认为主节点下线,才会进入故障转移。 + - 若配置是 `sentinel monitor mymaster ... 2`,则**两台哨兵都必须**认为主节点不可达。 + - 在**两台哨兵上分别**执行,看主节点是否已被判为客观下线(`flags` 里是否有 `o_down`): + ```bash + redis-cli -h <哨兵1_IP> -p 26379 -a <密码> SENTINEL master mymaster + redis-cli -h <哨兵2_IP> -p 26379 -a <密码> SENTINEL master mymaster + ``` + - 若只有一台认为 down,另一台仍认为 master 存活,则不会 ODOWN,也就不会触发选举。常见原因:网络分区、或两台哨兵到原主节点的网络表现不一致。 + +2. **是否有可提升的从节点** + 新主是从**当前从节点(replica)**里选出来的;若哨兵认为没有可用从节点,就不会选出新主。 + - 在两台哨兵上任选一台执行: + ```bash + redis-cli -h <哨兵IP> -p 26379 -a <密码> SENTINEL slaves mymaster + ``` + - 看列表是否为空、或从节点是否被标成 `s_down`/`o_down`。若从节点也被判下线或复制链路断开,哨兵不会提升它。 + +3. **法定人数与哨兵一致性** + 执行(在两台哨兵上都跑一次): + ```bash + redis-cli -h <哨兵IP> -p 26379 -a <密码> SENTINEL ckquorum mymaster + ``` + - 若提示 quorum 未满足,说明当前“认为主节点下线”的哨兵数不足,不会触发故障转移。 + +4. **quorum 配置是否过大** + 若当时部署时按 3 台哨兵配了 `quorum=2`,现在只剩 2 台,理论上 2 台都同意即可达到 quorum=2。但若误配成 **quorum=3**,则 2 台永远达不到 3,**不会**触发客观下线和选举。检查每台哨兵的 `sentinel.conf` 里 `sentinel monitor mymaster ...` 最后一个数字是否为 2(2 台哨兵时建议为 2)。 + +5. **等待时间** + 主节点需在 **down-after-milliseconds**(如 30000)内一直被判定不可达,才会主观下线,再经 quorum 达成客观下线。刚宕机后需至少等够这段时间再看是否选出新主。 + +**小结**:两哨兵存活仍不选主,多半是 **(1) quorum 未达成**(两台对主节点状态看法不一致)或 **(2) quorum 配成 3**,或 **(3) 从节点被哨兵判为不可用**。先查 `SENTINEL master mymaster` 的 flags、`SENTINEL slaves mymaster` 和 `SENTINEL ckquorum mymaster`,再对照上述几条处理。 --- @@ -256,8 +333,8 @@ spring: # --- DB --- database: ${app.redis-database:0} # 选择 DB(0~15,取决于你的 redis 配置) - # --- 启动检查 / 发现 --- - checkSentinelsList: true # 启动时校验 Sentinel 列表(默认 true) + # --- 启动检查 / 发现(单哨兵宕机不影响业务必读)--- + checkSentinelsList: false # 建议 false:启动/刷新时不强制校验“全部”哨兵,任一只哨兵宕机不会导致客户端整体不可用(默认 true 会连所有哨兵,一个不通就报错) sentinelsDiscovery: true # 是否自动发现更多 sentinel(默认 true;Docker/NAT 环境可考虑关掉避免“拓扑污染”) dnsMonitoringInterval: 5000 # DNS 变更监控间隔 ms;-1 表示禁用(对域名方式接入有用) @@ -359,4 +436,116 @@ spring: --- +## 八、单哨兵宕机不影响业务(Redisson 配置) + +### 8.1 现象与原因 + +当**只停止一个哨兵节点**时,若出现: + +- `No route to host: 192.168.3.110/192.168.3.110:26379`(其中 110 为已停的哨兵) +- 或 `RedisReadonlyException: READONLY You can't write against a read only replica`(写到了已变为 replica 的旧 master) + +**主要原因**是 Redisson 的以下行为: + +| 配置项 | 默认值 | 导致“单哨兵挂则全挂”的原因 | +|--------|--------|----------------------------| +| **checkSentinelsList** | `true` | 启动或周期性刷新时会对**配置中的每一个**哨兵执行校验/连接;任一哨兵不可达(如 110 已停)会触发异常或阻塞,导致客户端无法正常从其余哨兵获取当前 master,进而整体不可用或拿到过期拓扑。 | + +`RedisReadonlyException` 通常表示**已经发生过故障转移**(当前写到的 192.168.3.200 已被降为 replica),而客户端因上述原因无法从存活的哨兵刷新到新 master,仍向旧主写,从而报只读。 + +### 8.2 推荐配置(单哨兵宕机不影响业务) + +在 Nacos/应用配置中,将 Redisson 的哨兵相关配置改为: + +```yaml +sentinelServersConfig: + masterName: "mymaster" + sentinelAddresses: + - "redis://192.168.3.230:26379" + - "redis://192.168.3.200:26379" + - "redis://192.168.3.110:26379" + + # 关键:单哨兵宕机时仍可从其余哨兵获取 master,不影响业务 + checkSentinelsList: false # 不强制校验“全部”哨兵,任一只不可达不会拖垮客户端 + sentinelsDiscovery: true # 按需保留;Docker/NAT 环境若出现拓扑混乱可改为 false +``` + +**要点**: + +- **checkSentinelsList: false**:不再要求能连上配置里的每一个哨兵,只要有一个哨兵可用即可获取 master 并刷新拓扑,单哨兵停机不会导致业务不可用。 +- 保留多个 `sentinelAddresses`:多个哨兵仍可做高可用,只是客户端不会因“其中一个连不上”而整体失败。 + +### 8.3 为什么 SENTINEL masters 显示 200 却无法写入? + +`SENTINEL masters` 列出的是哨兵**当前记录的主节点信息**,但在以下情况下会出现“查到的是 200,却写不进去”: + +1. **200 已被降级为副本** + 故障转移后,192.168.3.200 可能已经执行了 `REPLICAOF` 指向新主,自身变为只读副本。此时 `SENTINEL masters` 里可能仍显示 `ip 192.168.3.200`(视图未刷新或不同哨兵收敛有先后),但连上 200 执行写会报 `READONLY`。 + +2. **应看“当前主节点”而非仅看 masters 表** + 用下面命令取的是**当前可写主节点**,比 `SENTINEL masters` 更贴近实际: + + ```bash + redis-cli -h 192.168.3.230 -p 26379 -a afe123456 SENTINEL get-master-addr-by-name mymaster + ``` + + 若返回的是**另一台**(例如 192.168.3.xxx),说明新主已切换,应连该地址写。 + +3. **在 200 上自检角色** + 直连 200(你环境里 Redis 端口为 16379): + + ```bash + redis-cli -h 192.168.3.200 -p 16379 -a afe123456 ROLE + ``` + + 若返回 `slave`,说明 200 已是副本,只能读不能写;写请求应发往 `get-master-addr-by-name` 返回的那台。 + +**小结**:查到是 200 却不能写,多半是 200 已是 replica。用 `SENTINEL get-master-addr-by-name mymaster` 确认当前主,并连该主写;应用侧在 `checkSentinelsList: false` 且能连上任意存活哨兵时会自动刷新到新主。 + +### 8.4 故障转移后的核对 + +若已出现 `s_down,o_down` 或 `role-reported: slave`,说明哨兵已做过故障转移。建议在**仍存活的**哨兵上确认当前主节点: + +```bash +redis-cli -h 192.168.3.230 -p 26379 -a afe123456 SENTINEL get-master-addr-by-name mymaster +``` + +应用在能连上任意存活哨兵且 `checkSentinelsList: false` 时,会通过拓扑刷新自动指向新 master,无需改配置。 + +### 8.5 双节点均为 slave(复制环)的排查与处理 + +当两台 Redis 主机在 `ROLE` 下均显示为 `slave`,且互相把对方当作 master 时,形成**复制环**:没有真正的主节点,所有写请求都会报 `READONLY`。 + +**典型表现**(示例 IP/端口可替换为实际环境): +- 192.168.3.200:16379 执行 `ROLE` → `slave`,master 指向 192.168.3.230:6379 +- 192.168.3.230:6379 执行 `ROLE` → `slave`,master 指向 192.168.3.200:16379 + +常见原因:多次故障转移、手动改过 `replicaof`、或 Sentinel 在异常情况下写入了错误的主从关系。 + +**处理步骤(人工指定主从)**: + +1. **选定一台作为唯一 master**(示例以 192.168.3.230 为主,端口按实际为准) + 在该节点上执行: + ```bash + redis-cli -h 192.168.3.230 -p 6379 -a <密码> REPLICAOF NO ONE + ``` + 执行后该节点成为唯一可写主节点。 + +2. **将另一台设为该主的从节点**(示例为 192.168.3.200 作为 230 的从) + 在从节点上执行(端口与上一步一致): + ```bash + redis-cli -h 192.168.3.200 -p 16379 -a <密码> REPLICAOF 192.168.3.230 6379 + ``` + +3. **校验** + - 在主节点上执行 `ROLE`,应看到 `master`。 + - 在从节点上执行 `ROLE`,应看到 `slave`,且 master 为刚指定的主节点地址与端口。 + +4. **哨兵** + Sentinel 会通过心跳发现新的主从拓扑;若长时间未更新,可在当前主节点所在机器重启该机 Sentinel,或按需执行 `SENTINEL failover`(拓扑已正确时一般无需执行)。 + +**注意**:端口需与现网一致(如 200 使用 16379、230 使用 6379 时,上述命令中的端口要对应修改)。 + +--- + **参考资料**:[Redisson 官方配置文档](https://redisson.pro/docs/configuration/#sentinel-yaml-config-format) \ No newline at end of file diff --git a/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-314.pyc b/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-314.pyc index 7b640b2..0866e97 100644 Binary files a/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-314.pyc and b/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-314.pyc differ diff --git a/skills/skill-creator/scripts/init_skill.py b/skills/skill-creator/scripts/init_skill.py index 329ad4e..22f8051 100644 --- a/skills/skill-creator/scripts/init_skill.py +++ b/skills/skill-creator/scripts/init_skill.py @@ -207,15 +207,15 @@ def init_skill(skill_name, path): # Check if directory already exists if skill_dir.exists(): - print(f"❌ Error: Skill directory already exists: {skill_dir}") + print(f"Error: Skill directory already exists: {skill_dir}") return None # Create skill directory try: skill_dir.mkdir(parents=True, exist_ok=False) - print(f"✅ Created skill directory: {skill_dir}") + print(f"Created skill directory: {skill_dir}") except Exception as e: - print(f"❌ Error creating directory: {e}") + print(f"Error creating directory: {e}") return None # Create SKILL.md from template @@ -227,10 +227,10 @@ def init_skill(skill_name, path): skill_md_path = skill_dir / 'SKILL.md' try: - skill_md_path.write_text(skill_content) - print("✅ Created SKILL.md") + skill_md_path.write_text(skill_content, encoding='utf-8') + print("Created SKILL.md") except Exception as e: - print(f"❌ Error creating SKILL.md: {e}") + print(f"Error creating SKILL.md: {e}") return None # Create resource directories with example files @@ -239,29 +239,29 @@ def init_skill(skill_name, path): scripts_dir = skill_dir / 'scripts' scripts_dir.mkdir(exist_ok=True) example_script = scripts_dir / 'example.py' - example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) + example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name), encoding='utf-8') example_script.chmod(0o755) - print("✅ Created scripts/example.py") + print("Created scripts/example.py") # Create references/ directory with example reference doc references_dir = skill_dir / 'references' references_dir.mkdir(exist_ok=True) example_reference = references_dir / 'api_reference.md' - example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) - print("✅ Created references/api_reference.md") + example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title), encoding='utf-8') + print("Created references/api_reference.md") # Create assets/ directory with example asset placeholder assets_dir = skill_dir / 'assets' assets_dir.mkdir(exist_ok=True) example_asset = assets_dir / 'example_asset.txt' - example_asset.write_text(EXAMPLE_ASSET) - print("✅ Created assets/example_asset.txt") + example_asset.write_text(EXAMPLE_ASSET, encoding='utf-8') + print("Created assets/example_asset.txt") except Exception as e: - print(f"❌ Error creating resource directories: {e}") + print(f"Error creating resource directories: {e}") return None # Print next steps - print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}") + print(f"\nSkill '{skill_name}' initialized successfully at {skill_dir}") print("\nNext steps:") print("1. Edit SKILL.md to complete the TODO items and update the description") print("2. Customize or delete the example files in scripts/, references/, and assets/") @@ -287,7 +287,7 @@ def main(): skill_name = sys.argv[1] path = sys.argv[3] - print(f"🚀 Initializing skill: {skill_name}") + print(f"Initializing skill: {skill_name}") print(f" Location: {path}") print() diff --git a/skills/skill-creator/scripts/package_skill.py b/skills/skill-creator/scripts/package_skill.py index 5cd36cb..9fd0a20 100644 --- a/skills/skill-creator/scripts/package_skill.py +++ b/skills/skill-creator/scripts/package_skill.py @@ -45,13 +45,13 @@ def package_skill(skill_path, output_dir=None): return None # Run validation before packaging - print("🔍 Validating skill...") + print("Validating skill...") valid, message = validate_skill(skill_path) if not valid: - print(f"❌ Validation failed: {message}") + print(f"Validation failed: {message}") print(" Please fix the validation errors before packaging.") return None - print(f"✅ {message}\n") + print(f"{message}\n") # Determine output location skill_name = skill_path.name @@ -74,11 +74,11 @@ def package_skill(skill_path, output_dir=None): zipf.write(file_path, arcname) print(f" Added: {arcname}") - print(f"\n✅ Successfully packaged skill to: {skill_filename}") + print(f"\nSuccessfully packaged skill to: {skill_filename}") return skill_filename except Exception as e: - print(f"❌ Error creating .skill file: {e}") + print(f"Error creating .skill file: {e}") return None @@ -93,7 +93,7 @@ def main(): skill_path = sys.argv[1] output_dir = sys.argv[2] if len(sys.argv) > 2 else None - print(f"📦 Packaging skill: {skill_path}") + print(f"Packaging skill: {skill_path}") if output_dir: print(f" Output directory: {output_dir}") print() diff --git a/skills/skill-creator/scripts/quick_validate.py b/skills/skill-creator/scripts/quick_validate.py index d9fbeb7..e20502d 100644 --- a/skills/skill-creator/scripts/quick_validate.py +++ b/skills/skill-creator/scripts/quick_validate.py @@ -19,7 +19,7 @@ def validate_skill(skill_path): return False, "SKILL.md not found" # Read and validate frontmatter - content = skill_md.read_text() + content = skill_md.read_text(encoding='utf-8') if not content.startswith('---'): return False, "No YAML frontmatter found"