This commit is contained in:
2026-03-17 17:20:34 +08:00
13 changed files with 960 additions and 24 deletions
+2
View File
@@ -0,0 +1,2 @@
# 勿提交含 API Token 的配置文件
jira_upload.env
+164
View File
@@ -0,0 +1,164 @@
---
name: g3fo-commit-jira
description: G3FO 项目 Git 提交规范自动化工具。强制要求提供 git 修订号(hash),支持用户直接提供 Jira 编号或自动查找/创建 Jira 任务。按任务类型(漏洞修复/新功能/重构等)判定需附加的文档(开发计划、影响分析、任务摘要),生成对应 MD 并按公司规范命名(<JIRA-ID>_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 <hash>` 检查修订号是否存在。
- **错误处理**:如果修订号无效或找不到,**必须** 停止操作并提示用户:“找不到修订号 `<hash>`,请提供正确的 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 <hash> # 获取 diff + 元数据
git log -1 --format="%B" <hash> # 获取原始 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 编号后再以 `<JIRA-ID>_*.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 文件名必须为
`<JIRA-ID>_Dev_Plan.md`、`<JIRA-ID>_Impact_Analysis.md`、`<JIRA-ID>_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: <git log --oneline -1 的结果>
```
---
## 相关参考
- **公司 Jira 附件文档政策**(何时附加哪些文件):`references/jira_commit_docs_policy.md`
- **报告模板、三文档说明与错误处理**:`references/workflow.md`
- **上传附件配置**:`references/env_config.md`;脚本 `scripts/upload_attachment.py`,依赖见 `scripts/requirements.txt`
@@ -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
@@ -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。
@@ -0,0 +1,48 @@
# Jira 工单附件文档政策(公司强制性指南)
自 **2026年3月9日** 起,以下为将 **开发计划**、**影响分析**、**任务摘要** 附加到 Jira 工单的强制性指南,用于跟踪设计决策、评估风险、为未来参考提供背景及项目间协作。
---
## 一般规则(经验法则)
| 场景 | 需要附加的文档 |
|------|----------------|
| **漏洞修复** | 仅包括 **影响分析**(若修复有非简单副作用) |
| **新功能 / 重大核心变更** | **全部三个文件**:开发计划、影响分析、任务摘要 |
| **琐碎 / 小改动** | 无需文件 |
---
## 详细条件(按任务类型)
| 任务类型 | 开发计划 | 影响分析 | 任务摘要 |
|----------|:--------:|:--------:|:--------:|
| 漏洞修复——简单(如拼写错误、界面对齐) | ❌ | ❌ | ❌ |
| 漏洞修复——中等(如范围有限的逻辑错误) | ❌ | ✅ | ❌ |
| 漏洞修复——复杂/高风险(如并发、核心模块) | ✅ | ✅ | ✅ |
| 小幅度增强(如添加小效用方法) | ❌ | ✅ | ❌ |
| 新功能/模块 | ✅ | ✅ | ✅ |
| 主要重构 | ✅ | ✅ | ✅ |
| 配置/基础设施变更 | ❌ | ✅ | ❌ |
---
## 文档含义说明
- **开发计划**:概述步骤、设计方法和技术决策。
- **影响分析**:列出受影响区域、风险、性能考虑因素及向后兼容性。
- **任务摘要**:简要回顾所完成的工作,包括任何偏离计划的地方。
---
## 提交方式与命名规范
- 将文件(Markdown)**直接放在工单的附件部分**。
- 使用一致的命名规范:
- `<JIRA-ID>_Dev_Plan.md`
- `<JIRA-ID>_Impact_Analysis.md`
- `<JIRA-ID>_Task_Summary.md`
示例:工单 `G3SF-123` 的三个附件命名为
`G3SF-123_Dev_Plan.md`、`G3SF-123_Impact_Analysis.md`、`G3SF-123_Task_Summary.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 必须命名为
`<JIRA-ID>_Dev_Plan.md`、`<JIRA-ID>_Impact_Analysis.md`、`<JIRA-ID>_Task_Summary.md`
例如:`G3SF-123_Impact_Analysis.md`。
- 根据任务类型仅生成并上传“需要”的文档;无需文档的琐碎改动仍可绑定 Jira 并 amend,但不生成报告、不写评论、不上传附件。
---
## Step 1: 获取并解析提交内容
### 命令
```powershell
# 检查 hash 是否为 HEAD
$head = git rev-parse HEAD
$provided = git rev-parse <hash>
if ($head -ne $provided) { Write-Error "提交不是 HEAD,无法 amend" }
# 获取完整 diff 和元数据
git show <hash>
# 仅获取原始 commit message(用于后续 amend 保留)
git log -1 --format="%B" <hash>
# 列出修改的文件(不含 diff 内容,快速预览)
git diff-tree --no-commit-id -r --name-status <hash>
```
### 解析要点
从 `git show` 输出中提取:
| 信息 | 说明 |
|------|------|
| `--- a/path` / `+++ b/path` | 修改的文件路径 |
| `@@ ... @@` 上下文 | 修改的类名、方法名 |
| `+` 行 | 新增代码(方法签名、接口、配置) |
| `-` 行 | 删除/替换代码 |
| 文件路径中的包名 | 推断所属服务(`g3fo-trade-service`, `g3fo-margin-service` 等) |
G3FO 服务包路径规律:
- `com.afe.g3fo.<service-short-name>.*` → 对应服务
- `controller` / `api` → REST 接口层
- `facade` / `facade.impl` → Dubbo 接口层
- `service` / `service.impl` → 业务逻辑层
---
## Step 2: 任务类型判定与三文档模板
### 2.1 按任务类型确定需生成的文档
| 任务类型 | 开发计划 | 影响分析 | 任务摘要 |
|----------|:--------:|:--------:|:--------:|
| 漏洞修复——简单(如拼写、界面对齐) | ❌ | ❌ | ❌ |
| 漏洞修复——中等(范围有限的逻辑错误) | ❌ | ✅ | ❌ |
| 漏洞修复——复杂/高风险(并发、核心模块) | ✅ | ✅ | ✅ |
| 小幅度增强(如小效用方法) | ❌ | ✅ | ❌ |
| 新功能/模块 | ✅ | ✅ | ✅ |
| 主要重构 | ✅ | ✅ | ✅ |
| 配置/基础设施变更 | ❌ | ✅ | ❌ |
生成内容后,在**取得 Jira 编号后**按公司规范保存为 `<JIRA-ID>_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 生成的各文档内容分别写入本地文件,文件名必须为:
- `<JIRA-ID>_Dev_Plan.md`
- `<JIRA-ID>_Impact_Analysis.md`
- `<JIRA-ID>_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>` 报错 | **停止**,提示用户提供正确的修订号。 |
| 提交 hash 不是 HEAD | `git rev-parse HEAD` ≠ `git rev-parse <hash>` | 停止,提示用户确认 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`。
@@ -0,0 +1 @@
requests>=2.28.0
@@ -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())
@@ -230,6 +230,83 @@ sentinel deny-scripts-reconfig yes
| `sentinel failover-timeout` | 故障转移超时时间 | `180000` | | `sentinel failover-timeout` | 故障转移超时时间 | `180000` |
| `sentinel announce-ip` | 宣告哨兵外部 IP | 宿主机实际 IP | | `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 <ip> <port> <quorum>` 的最后一个数字,例如:
```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 --- # --- DB ---
database: ${app.redis-database:0} # 选择 DB(0~15,取决于你的 redis 配置) database: ${app.redis-database:0} # 选择 DB(0~15,取决于你的 redis 配置)
# --- 启动检查 / 发现 --- # --- 启动检查 / 发现(单哨兵宕机不影响业务必读)---
checkSentinelsList: true # 启动时校验 Sentinel 列表(默认 true) checkSentinelsList: false # 建议 false:启动/刷新时不强制校验“全部”哨兵,任一只哨兵宕机不会导致客户端整体不可用(默认 true 会连所有哨兵,一个不通就报错)
sentinelsDiscovery: true # 是否自动发现更多 sentinel(默认 true;Docker/NAT 环境可考虑关掉避免“拓扑污染”) sentinelsDiscovery: true # 是否自动发现更多 sentinel(默认 true;Docker/NAT 环境可考虑关掉避免“拓扑污染”)
dnsMonitoringInterval: 5000 # DNS 变更监控间隔 ms;-1 表示禁用(对域名方式接入有用) 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) **参考资料**:[Redisson 官方配置文档](https://redisson.pro/docs/configuration/#sentinel-yaml-config-format)
+15 -15
View File
@@ -207,15 +207,15 @@ def init_skill(skill_name, path):
# Check if directory already exists # Check if directory already exists
if skill_dir.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 return None
# Create skill directory # Create skill directory
try: try:
skill_dir.mkdir(parents=True, exist_ok=False) 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: except Exception as e:
print(f"❌ Error creating directory: {e}") print(f"Error creating directory: {e}")
return None return None
# Create SKILL.md from template # Create SKILL.md from template
@@ -227,10 +227,10 @@ def init_skill(skill_name, path):
skill_md_path = skill_dir / 'SKILL.md' skill_md_path = skill_dir / 'SKILL.md'
try: try:
skill_md_path.write_text(skill_content) skill_md_path.write_text(skill_content, encoding='utf-8')
print("✅ Created SKILL.md") print("Created SKILL.md")
except Exception as e: except Exception as e:
print(f"❌ Error creating SKILL.md: {e}") print(f"Error creating SKILL.md: {e}")
return None return None
# Create resource directories with example files # Create resource directories with example files
@@ -239,29 +239,29 @@ def init_skill(skill_name, path):
scripts_dir = skill_dir / 'scripts' scripts_dir = skill_dir / 'scripts'
scripts_dir.mkdir(exist_ok=True) scripts_dir.mkdir(exist_ok=True)
example_script = scripts_dir / 'example.py' 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) example_script.chmod(0o755)
print("✅ Created scripts/example.py") print("Created scripts/example.py")
# Create references/ directory with example reference doc # Create references/ directory with example reference doc
references_dir = skill_dir / 'references' references_dir = skill_dir / 'references'
references_dir.mkdir(exist_ok=True) references_dir.mkdir(exist_ok=True)
example_reference = references_dir / 'api_reference.md' example_reference = references_dir / 'api_reference.md'
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title), encoding='utf-8')
print("✅ Created references/api_reference.md") print("Created references/api_reference.md")
# Create assets/ directory with example asset placeholder # Create assets/ directory with example asset placeholder
assets_dir = skill_dir / 'assets' assets_dir = skill_dir / 'assets'
assets_dir.mkdir(exist_ok=True) assets_dir.mkdir(exist_ok=True)
example_asset = assets_dir / 'example_asset.txt' example_asset = assets_dir / 'example_asset.txt'
example_asset.write_text(EXAMPLE_ASSET) example_asset.write_text(EXAMPLE_ASSET, encoding='utf-8')
print("✅ Created assets/example_asset.txt") print("Created assets/example_asset.txt")
except Exception as e: except Exception as e:
print(f"❌ Error creating resource directories: {e}") print(f"Error creating resource directories: {e}")
return None return None
# Print next steps # 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("\nNext steps:")
print("1. Edit SKILL.md to complete the TODO items and update the description") 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/") print("2. Customize or delete the example files in scripts/, references/, and assets/")
@@ -287,7 +287,7 @@ def main():
skill_name = sys.argv[1] skill_name = sys.argv[1]
path = sys.argv[3] path = sys.argv[3]
print(f"🚀 Initializing skill: {skill_name}") print(f"Initializing skill: {skill_name}")
print(f" Location: {path}") print(f" Location: {path}")
print() print()
@@ -45,13 +45,13 @@ def package_skill(skill_path, output_dir=None):
return None return None
# Run validation before packaging # Run validation before packaging
print("🔍 Validating skill...") print("Validating skill...")
valid, message = validate_skill(skill_path) valid, message = validate_skill(skill_path)
if not valid: if not valid:
print(f"❌ Validation failed: {message}") print(f"Validation failed: {message}")
print(" Please fix the validation errors before packaging.") print(" Please fix the validation errors before packaging.")
return None return None
print(f"✅ {message}\n") print(f"{message}\n")
# Determine output location # Determine output location
skill_name = skill_path.name skill_name = skill_path.name
@@ -74,11 +74,11 @@ def package_skill(skill_path, output_dir=None):
zipf.write(file_path, arcname) zipf.write(file_path, arcname)
print(f" Added: {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 return skill_filename
except Exception as e: except Exception as e:
print(f"❌ Error creating .skill file: {e}") print(f"Error creating .skill file: {e}")
return None return None
@@ -93,7 +93,7 @@ def main():
skill_path = sys.argv[1] skill_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else None 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: if output_dir:
print(f" Output directory: {output_dir}") print(f" Output directory: {output_dir}")
print() print()
@@ -19,7 +19,7 @@ def validate_skill(skill_path):
return False, "SKILL.md not found" return False, "SKILL.md not found"
# Read and validate frontmatter # Read and validate frontmatter
content = skill_md.read_text() content = skill_md.read_text(encoding='utf-8')
if not content.startswith('---'): if not content.startswith('---'):
return False, "No YAML frontmatter found" return False, "No YAML frontmatter found"