Merge branch 'master' into zk_branch
- resolve conflicts in register.go, release/release.go, repo/repo.go, SKILL.md - keep milestone/team/webhook registrations from zk_branch - incorporate batch_create/batch_update from master Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
d122b95d6c
|
|
@ -0,0 +1,78 @@
|
|||
version: 2
|
||||
name: gitlink-cli-autodeploy
|
||||
description: "每次push到master时自动在服务器上构建并部署gitlink-cli"
|
||||
global:
|
||||
concurrent: 1
|
||||
|
||||
trigger:
|
||||
webhook: gitlink@1.0.0
|
||||
event:
|
||||
- ref: push
|
||||
ruleset-operator: AND
|
||||
condition:
|
||||
param: ref
|
||||
operator: include_regex
|
||||
value: "^refs/heads/master$"
|
||||
|
||||
workflow:
|
||||
- ref: start
|
||||
name: 开始
|
||||
task: start
|
||||
|
||||
# 1. 准备服务器环境
|
||||
- ref: prepare_server_0
|
||||
name: 准备服务器环境
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.222.0"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_private_key: ((gitlink_cli.gitlink_cli_deploy_key))
|
||||
ssh_cmd: '"/opt/gitlink-deploy/prepare.sh"'
|
||||
needs:
|
||||
- start
|
||||
|
||||
# 2. 在服务器上构建
|
||||
- ref: build_on_server_0
|
||||
name: 在服务器上构建
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.222.0"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_private_key: ((gitlink_cli.gitlink_cli_deploy_key))
|
||||
ssh_cmd: '"/opt/gitlink-deploy/build.sh"'
|
||||
needs:
|
||||
- prepare_server_0
|
||||
|
||||
# 3. 部署到服务器
|
||||
- ref: deploy_on_server_0
|
||||
name: 部署到服务器
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.222.0"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_private_key: ((gitlink_cli.gitlink_cli_deploy_key))
|
||||
ssh_cmd: '"/opt/gitlink-deploy/deploy.sh"'
|
||||
needs:
|
||||
- build_on_server_0
|
||||
|
||||
# 4. 验证部署
|
||||
- ref: verify_deployment_0
|
||||
name: 验证部署
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.222.0"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_private_key: ((gitlink_cli.gitlink_cli_deploy_key))
|
||||
ssh_cmd: '"gitlink-cli version && ls -lh /opt/gitlink-cli/gitlink-cli"'
|
||||
needs:
|
||||
- deploy_on_server_0
|
||||
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- verify_deployment_0
|
||||
55
README.md
55
README.md
|
|
@ -27,10 +27,12 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| Category | Capabilities |
|
||||
|----------|-------------|
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info |
|
||||
| 🐛 Issue | Create, update, close, batch close, comment on issues |
|
||||
| 🐛 Issue | Create, update, close, comment on issues, 6 batch operations (close/status/priority/assignee/label/create) |
|
||||
| 📖 Wiki | View, create, update, delete Wiki pages |
|
||||
| 🔀 PR | Create, merge, review pull requests, view changed files |
|
||||
| 🌿 Branch | Create, delete, list, protect, unprotect branches |
|
||||
| 🏷️ Release | Create, view, delete releases |
|
||||
| 🔗 Webhook | Create, view, update, delete, test webhooks, configure automation triggers |
|
||||
| 🏢 Org | Manage organizations, members, teams |
|
||||
| 🔧 CI | View builds, logs, CI/CD operations |
|
||||
| 🔍 Search | Search repositories, users |
|
||||
|
|
@ -209,6 +211,34 @@ gitlink-cli branch +protect --name main
|
|||
gitlink-cli branch +unprotect --name main
|
||||
```
|
||||
|
||||
### Webhook Management
|
||||
|
||||
```bash
|
||||
# List all webhooks
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Create a webhook
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus --url https://ci.example.com/webhook --events push,pull_request
|
||||
|
||||
# Create webhook with secret
|
||||
gitlink-cli webhook +create --url https://jenkins.example.com/webhook --secret my-secret-key --events push --description "CI/CD trigger"
|
||||
|
||||
# View webhook details
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# Update webhook
|
||||
gitlink-cli webhook +update --id 456 --url https://new-url.example.com/webhook --events push,pull_request,issue
|
||||
|
||||
# Test webhook
|
||||
gitlink-cli webhook +test --id 456
|
||||
|
||||
# Delete webhook
|
||||
gitlink-cli webhook +delete --id 456
|
||||
|
||||
# List supported event types
|
||||
gitlink-cli webhook +events
|
||||
```
|
||||
|
||||
### Release Management
|
||||
|
||||
```bash
|
||||
|
|
@ -235,6 +265,28 @@ gitlink-cli ci +log --owner Gitlink --repo forgeplus -i <build_id>
|
|||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i <build_id>
|
||||
```
|
||||
|
||||
### Wiki Management
|
||||
|
||||
```bash
|
||||
# List wiki pages
|
||||
gitlink-cli wiki +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# View a wiki page
|
||||
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "Getting Started"
|
||||
|
||||
# Create a wiki page
|
||||
gitlink-cli wiki +create --title "New Page" --content "Page content"
|
||||
|
||||
# Update a wiki page (overwrite)
|
||||
gitlink-cli wiki +update --title "New Page" --cover "Updated content" --owner Gitlink --repo forgeplus
|
||||
|
||||
# Append content to a wiki page
|
||||
gitlink-cli wiki +update --title "New Page" --add "Appended content" --owner Gitlink --repo forgeplus
|
||||
|
||||
# Delete a wiki page
|
||||
gitlink-cli wiki +delete --title "New Page" --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
|
|
@ -339,6 +391,7 @@ gitlink-cli/
|
|||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── search/ # Search shortcuts
|
||||
│ ├── user/ # User shortcuts
|
||||
│ ├── wiki/ # Wiki shortcuts
|
||||
│ └── register.go # Registration entry point
|
||||
├── skills/ # AI Agent Skills
|
||||
│ ├── README.md # Skills guide
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@
|
|||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
|
||||
| 🐛 Issue | 创建、更新、关闭、评论 Issue,6 个批量操作(关闭/状态/优先级/负责人/标记/创建) |
|
||||
| 📖 Wiki | 查看、创建、更新、删除 Wiki 页面 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🏷️ 发布 | 创建、查看、删除 Release |
|
||||
| 🔗 Webhook | 创建、查看、更新、删除、测试 Webhook,配置自动化触发器 |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
|
|
@ -153,6 +155,12 @@ gitlink-cli repo +create -n my-project -d "项目描述"
|
|||
|
||||
# Fork 仓库
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
|
||||
# 批量创建仓库(默认公开,--private设置为私有)
|
||||
gitlink-cli repo +batch-create -n "repo1,repo2" -d "项目描述"
|
||||
|
||||
# 批量更新仓库信息(--private设置为私有,--public设置为公开,注意要指定仓库所有者)
|
||||
gitlink-cli repo +batch-update -n "repo1,repo2" -d "更新描述"
|
||||
```
|
||||
|
||||
### Issue 管理
|
||||
|
|
@ -178,6 +186,25 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.cs
|
|||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
|
||||
|
||||
# 批量修改状态
|
||||
gitlink-cli issue +batch-status --state resolved --numbers 1,2,3 --owner Gitlink --repo forgeplus
|
||||
|
||||
# 批量修改优先级
|
||||
gitlink-cli issue +batch-priority --priority urgent --numbers 1,2,3 --owner Gitlink --repo forgeplus
|
||||
|
||||
# 批量修改标签
|
||||
gitlink-cli issue +batch-label --numbers 1,2,3 --label 功能 --owner Gitlink --repo forgeplus
|
||||
|
||||
# 批量修改负责人
|
||||
gitlink-cli issue +batch-assignee --numbers 1,2,3 --assignee zhangsan --owner Gitlink --repo forgeplus
|
||||
|
||||
# 批量创建
|
||||
gitlink-cli issue --owner Gitlink --repo forgeplus +batch-create --titles "issue1,issue2,issue3"
|
||||
|
||||
# 批量关闭
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 1,2,3
|
||||
|
||||
```
|
||||
|
||||
### Pull Request
|
||||
|
|
@ -202,6 +229,34 @@ gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42
|
|||
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
|
||||
```
|
||||
|
||||
### Webhook 管理
|
||||
|
||||
```bash
|
||||
# 列出所有 Webhook
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 Webhook
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus --url https://ci.example.com/webhook --events push,pull_request
|
||||
|
||||
# 创建带密钥的 Webhook
|
||||
gitlink-cli webhook +create --url https://jenkins.example.com/webhook --secret my-secret-key --events push --description "CI/CD trigger"
|
||||
|
||||
# 查看 Webhook 详情
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 更新 Webhook
|
||||
gitlink-cli webhook +update --id 456 --url https://new-url.example.com/webhook --events push,pull_request,issue
|
||||
|
||||
# 测试 Webhook
|
||||
gitlink-cli webhook +test --id 456
|
||||
|
||||
# 删除 Webhook
|
||||
gitlink-cli webhook +delete --id 456
|
||||
|
||||
# 查看支持的事件类型
|
||||
gitlink-cli webhook +events
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
||||
```bash
|
||||
|
|
@ -215,6 +270,28 @@ gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.
|
|||
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
|
||||
```
|
||||
|
||||
### Wiki 管理
|
||||
|
||||
```bash
|
||||
# 列出 Wiki 页面
|
||||
gitlink-cli wiki +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看 Wiki 页面
|
||||
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "快速开始"
|
||||
|
||||
# 创建 Wiki 页面
|
||||
gitlink-cli wiki +create --title "新页面" --content "页面正文"
|
||||
|
||||
# 更新 Wiki 页面(覆盖内容)
|
||||
gitlink-cli wiki +update --title "新页面" --cover "更新后的内容" --owner Gitlink --repo forgeplus
|
||||
|
||||
# 追加内容到 Wiki 页面
|
||||
gitlink-cli wiki +update --title "新页面" --add "追加的内容" --owner Gitlink --repo forgeplus
|
||||
|
||||
# 删除 Wiki 页面
|
||||
gitlink-cli wiki +delete --title "新页面" --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### 搜索
|
||||
|
||||
```bash
|
||||
|
|
@ -318,6 +395,7 @@ gitlink-cli/
|
|||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── search/ # 搜索 shortcuts
|
||||
│ ├── user/ # 用户 shortcuts
|
||||
│ ├── wiki/ # Wiki shortcuts
|
||||
│ └── register.go # 注册入口
|
||||
├── skills/ # AI Agent Skills
|
||||
│ ├── README.md # Skills 使用指南
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ func runAPI(c *cobra.Command, args []string) error {
|
|||
env, err := cli.Do(method, path, body, query)
|
||||
if err != nil {
|
||||
if apiErr, ok := err.(*client.APIError); ok {
|
||||
errEnv := output.ErrorEnvelope(apiErr.Code, apiErr.Message, "")
|
||||
errEnv := output.ErrorEnvelope(apiErr.Code, apiErr.Message, apiErr.Suggestion)
|
||||
return output.Print(errEnv, resolveFormat())
|
||||
}
|
||||
return err
|
||||
return fmt.Errorf("API 请求失败 [%s %s]: %w", method, path, err)
|
||||
}
|
||||
|
||||
return output.Print(env, resolveFormat())
|
||||
|
|
|
|||
|
|
@ -44,19 +44,25 @@ func newLoginCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
func loginWithPassword() error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
// Check for credentials from environment variables for non-interactive login
|
||||
username := os.Getenv("GITLINK_USERNAME")
|
||||
password := os.Getenv("GITLINK_PASSWORD")
|
||||
|
||||
fmt.Print("Username/Email/Phone: ")
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" || password == "" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Password: ")
|
||||
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password: %w", err)
|
||||
fmt.Print("Username/Email/Phone: ")
|
||||
usernameInput, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(usernameInput)
|
||||
|
||||
fmt.Print("Password: ")
|
||||
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
password = string(passwordBytes)
|
||||
}
|
||||
fmt.Println()
|
||||
password := string(passwordBytes)
|
||||
|
||||
result, err := internalAuth.Login(username, password)
|
||||
if err != nil {
|
||||
|
|
|
|||
203
doc/design.md
203
doc/design.md
|
|
@ -40,8 +40,9 @@ gitlink-cli/
|
|||
│ ├── common/
|
||||
│ │ ├── types.go # Shortcut / Flag / RuntimeContext 定义
|
||||
│ │ └── runner.go # CallAPI / PaginateAll / ResolveOwnerRepo
|
||||
│ ├── repo/ # repo +create / +clone / +fork / +list / +info
|
||||
│ ├── issue/ # issue +list / +create / +view / +close / +comment
|
||||
│ ├── repo/ # repo +create / +clone / +fork / +list / +info / +batch-create / +batch-update
|
||||
│ ├── issue/ # issue +list / +create / +view / +close / +comment / +batch-* (6 个批量命令)
|
||||
│ ├── wiki/ # wiki +list / +view / +create / +update / +delete
|
||||
│ ├── pr/ # pr +list / +create / +view / +merge / +review
|
||||
│ ├── release/ # release +list / +create / +download
|
||||
│ ├── branch/ # branch +list / +protect / +unprotect
|
||||
|
|
@ -78,8 +79,9 @@ gitlink-cli/
|
|||
|
||||
| 领域 | Shortcuts | 数量 |
|
||||
|------|-----------|------|
|
||||
| repo | `+create` `+clone` `+fork` `+list` `+info` `+delete` `+settings` | 7 |
|
||||
| issue | `+list` `+create` `+view` `+update` `+close` `+comment` `+assign` `+label` | 8 |
|
||||
| repo | `+create` `+clone` `+fork` `+list` `+info` `+delete` `+settings` `+batch-create` `+batch-update` | 9 |
|
||||
| issue | `+list` `+create` `+view` `+update` `+close` `+comment` `+assign` `+label` `+batch-close` `+batch-status` `+batch-priority` `+batch-assign` `+batch-label` `+batch-create` | 14 |
|
||||
| wiki | `+list` `+view` `+create` `+update` `+delete` | 5 |
|
||||
| pr | `+list` `+create` `+view` `+merge` `+close` `+review` `+files` `+diff` | 8 |
|
||||
| release | `+list` `+create` `+view` `+delete` `+download` | 5 |
|
||||
| branch | `+list` `+create` `+delete` `+protect` `+unprotect` | 5 |
|
||||
|
|
@ -437,7 +439,169 @@ skills/
|
|||
|
||||
---
|
||||
|
||||
## 9 完整命令参考
|
||||
## 9 批量操作设计模式
|
||||
|
||||
Issue 和 Repo 两个领域均实现了批量操作命令,遵循统一的设计模式。
|
||||
|
||||
### 9.1 命令清单
|
||||
|
||||
| 领域 | 命令 | 用途 | 输入方式 |
|
||||
|------|------|------|----------|
|
||||
| issue | `+batch-close` | 批量关闭 | `--numbers` 或 `--from` CSV |
|
||||
| issue | `+batch-status` | 批量更换状态 | `--state` + `--numbers`/`--from` |
|
||||
| issue | `+batch-priority` | 批量更换优先级 | `--priority` + `--numbers`/`--from` |
|
||||
| issue | `+batch-assign` | 批量更换负责人 | `--assignee` + `--numbers`/`--from` |
|
||||
| issue | `+batch-label` | 批量更换标记 | `--label` + `--numbers`/`--from` |
|
||||
| issue | `+batch-create` | 批量创建 Issue | `--titles` 或 `--from` CSV(支持 Bug/Feature 模板) |
|
||||
| repo | `+batch-create` | 批量创建仓库 | `--names` 或 `--from` CSV |
|
||||
| repo | `+batch-update` | 批量更新仓库 | `--names` 或 `--from` CSV |
|
||||
|
||||
### 9.2 核心类型
|
||||
|
||||
```go
|
||||
type BatchResult struct {
|
||||
Number string `json:"number"` // Issue 编号或仓库名
|
||||
Action string `json:"action"` // 操作类型
|
||||
Status string `json:"status"` // 执行结果
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type BatchSummary struct {
|
||||
Repository string `json:"repository"`
|
||||
Action string `json:"action"`
|
||||
Value string `json:"value,omitempty"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Results []BatchResult `json:"results"`
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 统一设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| **输入灵活** | `--numbers`/`--names`(内联逗号分隔)和 `--from`(CSV 文件)可同时使用,自动去重合并 |
|
||||
| **dry-run 统一** | 所有批量命令支持 `--dry-run`,预览模式下 status 为 `"planned"`,不发起写请求 |
|
||||
| **错误不中断** | 单条失败不影响后续处理,全部执行完后返回完整汇总。有任何失败则 exit code = 1 |
|
||||
| **输出统一** | 所有命令输出相同结构的 `BatchSummary` JSON |
|
||||
| **修改前先 GET** | Issue 批量修改先 GET 当前 Issue 保留 subject/description,PATCH 时只替换目标字段。Repo 批量更新先 GET 获取 name + identifier |
|
||||
| **参数容错** | 所有名称映射(状态、优先级、标签、负责人)同时支持字符串名和直接传数字 ID |
|
||||
|
||||
### 9.4 Issue 参数值映射
|
||||
|
||||
**状态**:`new`(1) / `in-progress`(2) / `resolved`(3) / `closed`(5) / `rejected`(6)
|
||||
|
||||
**优先级**:`low`(1) / `normal`(2) / `high`(3) / `urgent`(4)
|
||||
|
||||
**标记**:使用项目级中文标签名映射到大整数 ID(如 `缺陷`→315526、`功能`→315527)
|
||||
|
||||
**负责人**:传入 login 用户名,CLI 调用 `/users/{login}` API 转换为 `user_id`
|
||||
|
||||
### 9.5 Issue 批量创建模板系统
|
||||
|
||||
`+batch-create` 支持三种输入模式:
|
||||
|
||||
1. **CLI 直接输入**(`--titles`):逗号分隔标题,统一应用 `--priority`/`--label`/`--assignee`/`--state`
|
||||
2. **自由 CSV**(`--from`):自由指定 title/body/priority/label/assignee/status 列
|
||||
3. **模板 CSV**(`--from` + `--template`):
|
||||
- `--template bug`:自动生成 Bug 描述格式,自动设置缺陷标签
|
||||
- `--template feature`:自动生成功能描述格式,自动设置功能标签
|
||||
|
||||
### 9.6 Repo 批量操作注意事项
|
||||
|
||||
- **batch-create**:POST 路径 `/{login}/{name}` 中的 login 必须是当前登录用户,需先 `GET /users/me`
|
||||
- **batch-update**:PATCH 请求体必须包含从 GET 获取的 `name` 和 `identifier`,否则 API 报错
|
||||
- **--private/--public 互斥**:batch-update 不允许同时设置两个标志
|
||||
|
||||
### 9.7 关键 API 字段差异
|
||||
|
||||
GitLink 基于 Redmine 但修改了大量字段名:
|
||||
|
||||
| Redmine 标准字段 | GitLink 实际字段 | 格式 |
|
||||
|-----------------|-----------------|------|
|
||||
| `assigned_to_id` | `assigner_ids` | 数组 `[user_id]` |
|
||||
| `tracker_id` | `issue_tag_ids` | 数组 `[tag_id]`(项目级大整数) |
|
||||
|
||||
> GitLink API 对不认识的字段返回 200 而非报错,字段名错误会导致静默失败。必须通过浏览器 DevTools 抓取实际请求确认字段名和格式。
|
||||
|
||||
---
|
||||
|
||||
## 10 Wiki Shortcut 设计
|
||||
|
||||
Wiki 是独立的全新 Shortcut 领域,提供 5 个命令覆盖 Wiki 页面的 CRUD 操作。
|
||||
|
||||
### 10.1 双域名架构
|
||||
|
||||
Wiki API 部署在 gateway 域名上,与主站 API(www 域名)分离:
|
||||
|
||||
```
|
||||
┌─ www 域名 ─────────────────────┐
|
||||
resolveProjectID() → │ GET /{owner}/{repo}/detail.json │ → project_id
|
||||
└────────────────────────────────┘
|
||||
|
||||
┌─ gateway 域名 ───────────────────────────┐
|
||||
callWikiAPI*() → │ /wiki/open/* (不带 .json) │ → 解包 code/data
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **www 域名**(`https://www.gitlink.org.cn/api`):用于 detail API 获取 project_id,走默认 client
|
||||
- **gateway 域名**(`https://gateway.gitlink.org.cn/api`):用于所有 wiki CRUD API,走独立 client
|
||||
|
||||
### 10.2 Client 扩展
|
||||
|
||||
`internal/client/client.go` 新增 `SkipJSONSuffix` 字段:
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
// ... 原有字段 ...
|
||||
SkipJSONSuffix bool // 为 true 时不自动追加 .json 后缀(wiki gateway API 需要)
|
||||
}
|
||||
```
|
||||
|
||||
wiki 命令创建独立 client 实例,设置 `SkipJSONSuffix: true` 并使用 gateway BaseURL。
|
||||
|
||||
### 10.3 响应解包
|
||||
|
||||
Gateway API 使用不同的响应格式 `{code, data, msg}`(而非常规的 `{status, ...}`):
|
||||
|
||||
```go
|
||||
// unwrapGatewayResponse 解包 gateway 响应
|
||||
// 成功: code=200/201, 提取 data 字段
|
||||
// 失败: code=500/400/404, 返回 "[code] msg" 错误信息
|
||||
func unwrapGatewayResponse(raw []byte) ([]byte, error)
|
||||
```
|
||||
|
||||
### 10.4 命令详情
|
||||
|
||||
| 命令 | HTTP 方法 | API 路径 | 关键参数 |
|
||||
|------|----------|---------|---------|
|
||||
| `wiki +list` | GET | `/wiki/open/wikiPages` | 无额外参数 |
|
||||
| `wiki +view` | GET | `/wiki/open/getWiki` | `--title`(必填) |
|
||||
| `wiki +create` | POST | `/wiki/open/createWiki` | `--title`(必填)`--content/--file` `--message` |
|
||||
| `wiki +update` | PUT | `/wiki/open/updateWiki` | `--title`(必填)`--cover/--add` `--file` |
|
||||
| `wiki +delete` | DELETE | `/wiki/open/deleteWiki` | `--title`(必填) |
|
||||
|
||||
### 10.5 特殊处理
|
||||
|
||||
| 处理项 | 说明 |
|
||||
|--------|------|
|
||||
| **base64 编解码** | Wiki 内容在 API 中为 base64 编码,CLI 自动编解码,对用户透明 |
|
||||
| **project_id 缓存** | `resolveProjectID()` 使用 `sync.Map` 缓存,同一 owner/repo 只调一次 API |
|
||||
| **owner/repo 自动解析** | 在 git 仓库目录下可省略 `--owner`/`--repo` |
|
||||
| **--update --add 模式** | 先 GET 现有内容 → 解码 → 追加 → 重新编码 → PUT 提交 |
|
||||
| **嵌套对象过滤** | 表格输出时过滤无意义的嵌套对象字段(如 wiki_clone_link) |
|
||||
|
||||
### 10.6 已知限制
|
||||
|
||||
- **delete 后端 bug**:GitLink 平台 `deleteWiki` API 只清空内容,不删除侧边栏条目
|
||||
- **gateway 域名硬编码**:wiki API 仅部署在 gateway,detail API 在 www,不可互换
|
||||
- **create/update pageName 差异**:create 接受原始中文 pageName,update 需要 URL 编码
|
||||
|
||||
---
|
||||
|
||||
## 11 完整命令参考
|
||||
|
||||
```
|
||||
gitlink-cli
|
||||
|
|
@ -457,7 +621,9 @@ gitlink-cli
|
|||
│ ├── +list # 仓库列表
|
||||
│ ├── +info # 仓库详情
|
||||
│ ├── +delete # 删除仓库
|
||||
│ └── +settings # 仓库设置
|
||||
│ ├── +settings # 仓库设置
|
||||
│ ├── +batch-create # 批量创建仓库
|
||||
│ └── +batch-update # 批量更新仓库
|
||||
├── issue
|
||||
│ ├── +list # Issue 列表
|
||||
│ ├── +create # 创建 Issue
|
||||
|
|
@ -466,7 +632,13 @@ gitlink-cli
|
|||
│ ├── +close # 关闭 Issue
|
||||
│ ├── +comment # 添加评论
|
||||
│ ├── +assign # 指派
|
||||
│ └── +label # 标签管理
|
||||
│ ├── +label # 标签管理
|
||||
│ ├── +batch-close # 批量关闭 Issue
|
||||
│ ├── +batch-status # 批量更换状态
|
||||
│ ├── +batch-priority # 批量更换优先级
|
||||
│ ├── +batch-assign # 批量更换负责人
|
||||
│ ├── +batch-label # 批量更换标记
|
||||
│ └── +batch-create # 批量创建 Issue(含 Bug/Feature 模板)
|
||||
├── pr
|
||||
│ ├── +list # PR 列表
|
||||
│ ├── +create # 创建 PR
|
||||
|
|
@ -501,6 +673,12 @@ gitlink-cli
|
|||
├── user
|
||||
│ ├── +me # 当前用户
|
||||
│ └── +info # 用户详情
|
||||
├── wiki
|
||||
│ ├── +list # Wiki 页面列表
|
||||
│ ├── +view # 查看 Wiki 页面
|
||||
│ ├── +create # 创建 Wiki 页面
|
||||
│ ├── +update # 更新 Wiki 页面
|
||||
│ └── +delete # 删除 Wiki 页面
|
||||
├── search
|
||||
│ ├── +repos # 搜索仓库
|
||||
│ ├── +issues # 搜索 Issue
|
||||
|
|
@ -518,7 +696,7 @@ gitlink-cli
|
|||
|
||||
---
|
||||
|
||||
## 10 关键文件清单
|
||||
## 12 关键文件清单
|
||||
|
||||
实现时需要修改/创建的核心文件:
|
||||
|
||||
|
|
@ -541,8 +719,9 @@ gitlink-cli
|
|||
| `internal/registry/meta_data.json` | API 元数据 |
|
||||
| `shortcuts/common/types.go` | Shortcut 核心类型 |
|
||||
| `shortcuts/common/runner.go` | RuntimeContext |
|
||||
| `shortcuts/repo/*.go` | 仓库 shortcuts |
|
||||
| `shortcuts/issue/*.go` | Issue shortcuts |
|
||||
| `shortcuts/repo/*.go` | 仓库 shortcuts(含 batch_create/batch_update) |
|
||||
| `shortcuts/issue/*.go` | Issue shortcuts(含 batch.go + batch_create.go 批量操作) |
|
||||
| `shortcuts/wiki/*.go` | Wiki shortcuts |
|
||||
| `shortcuts/pr/*.go` | PR shortcuts |
|
||||
| `shortcuts/register.go` | Shortcut 注册 |
|
||||
| `skills/gitlink-shared/SKILL.md` | 共享 Skill |
|
||||
|
|
@ -550,7 +729,7 @@ gitlink-cli
|
|||
|
||||
---
|
||||
|
||||
## 11 开发计划
|
||||
## 13 开发计划
|
||||
|
||||
### Phase 1: Foundation(第 1-2 周)
|
||||
|
||||
|
|
@ -609,7 +788,7 @@ gitlink-cli
|
|||
|
||||
---
|
||||
|
||||
## 12 验证方案
|
||||
## 14 验证方案
|
||||
|
||||
| 阶段 | 验证方式 |
|
||||
|------|----------|
|
||||
|
|
|
|||
|
|
@ -15206,6 +15206,46 @@ GET /api/wikiExport/wikiExport-wrapper
|
|||
|» data|object|false|none||none|
|
||||
|» message|string|false|none||none|
|
||||
|
||||
---
|
||||
|
||||
## Gateway Wiki API(CLI 实际调用)
|
||||
|
||||
gitlink-cli 的 wiki shortcut 实际调用的是 **gateway 域名**下的 `/wiki/open/*` 端点,而非上述 www 域名的 `/api/wiki/*` 端点。两者存在以下差异:
|
||||
|
||||
| 差异项 | www 域名(APIfox 文档) | gateway 域名(CLI 实际使用) |
|
||||
|--------|------------------------|---------------------------|
|
||||
| Base URL | `https://www.gitlink.org.cn/api` | `https://gateway.gitlink.org.cn/api` |
|
||||
| URL 前缀 | `/api/wiki/` | `/wiki/open/` |
|
||||
| JSON 后缀 | 需要 `.json` | 不需要 `.json` |
|
||||
| 响应格式 | 直接返回 data | `{"code": 200, "data": {...}, "msg": ""}` 包一层 |
|
||||
| 错误判断 | `status` 字段 ≠ 200 | `code` 字段 ≠ 200/201 |
|
||||
|
||||
### 实际调用路径对比
|
||||
|
||||
| 操作 | www 文档路径 | gateway 实际路径 |
|
||||
|------|-------------|-----------------|
|
||||
| 创建 | `POST /api/wiki/createWiki.json` | `POST /wiki/open/createWiki` |
|
||||
| 删除 | `DELETE /api/wiki/deleteWiki.json` | `DELETE /wiki/open/deleteWiki` |
|
||||
| 查看 | `GET /api/wiki/getWiki.json` | `GET /wiki/open/getWiki` |
|
||||
| 更新 | `PUT /api/wiki/updateWiki.json` | `PUT /wiki/open/updateWiki` |
|
||||
| 列表 | `GET /api/wiki/wikiPages.json` | `GET /wiki/open/wikiPages` |
|
||||
|
||||
### 响应格式差异示例
|
||||
|
||||
**www 域名响应**(标准格式):
|
||||
```json
|
||||
{"data": {"title": "test", "content": "..."}}
|
||||
```
|
||||
|
||||
**gateway 域名响应**(包一层):
|
||||
```json
|
||||
{"code": 200, "data": {"title": "test", "content": "..."}, "msg": "success"}
|
||||
```
|
||||
|
||||
CLI 通过 `unwrapGatewayResponse()` 函数自动解包 gateway 格式,对用户透明。
|
||||
|
||||
> **注意**:project_id 仍需通过 www 域名的 `/api/{owner}/{repo}/detail.json` 获取,两个域名不可互换。
|
||||
|
||||
# 流水线
|
||||
|
||||
## GET 流水线列表
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func Login(username, password string) (*LoginResult, error) {
|
|||
// Collect auth cookies from response (GitLink uses autologin_trustie for session persistence)
|
||||
var authCookies []string
|
||||
for _, cookie := range resp.Cookies() {
|
||||
if cookie.Name == "autologin_trustie" || cookie.Name == "_educoder_session" {
|
||||
if cookie.Name == "Authorization" || cookie.Name == "autologin_trustie" || cookie.Name == "_educoder_session" || cookie.Name == "autologin" || cookie.Name == "login" {
|
||||
authCookies = append(authCookies, cookie.Name+"="+cookie.Value)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ func Login(username, password string) (*LoginResult, error) {
|
|||
if len(authCookies) == 0 {
|
||||
if u, err := url.Parse(loginURL); err == nil {
|
||||
for _, cookie := range jar.Cookies(u) {
|
||||
if cookie.Name == "autologin_trustie" || cookie.Name == "_educoder_session" {
|
||||
if cookie.Name == "Authorization" || cookie.Name == "autologin_trustie" || cookie.Name == "_educoder_session" || cookie.Name == "autologin" || cookie.Name == "login" {
|
||||
authCookies = append(authCookies, cookie.Name+"="+cookie.Value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/auth"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/config"
|
||||
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ type APIError struct {
|
|||
StatusCode int
|
||||
Code interface{}
|
||||
Message string
|
||||
Kind clierrors.ErrorKind
|
||||
Suggestion string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
|
|
@ -101,10 +104,13 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
|
|||
|
||||
// Check HTTP-level errors
|
||||
if resp.StatusCode >= 400 {
|
||||
info := lookupStatusInfo(resp.StatusCode)
|
||||
return nil, &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Code: resp.StatusCode,
|
||||
Message: fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))),
|
||||
Kind: info.kind,
|
||||
Suggestion: info.suggestion,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,11 +132,13 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
|
|||
}
|
||||
if statusCode != 0 && statusCode != 200 && statusCode != 1 {
|
||||
msg, _ := raw["message"].(string)
|
||||
suggestion := suggestFix(int(statusCode))
|
||||
return output.ErrorEnvelope(int(statusCode), msg, suggestion), &APIError{
|
||||
info := lookupStatusInfo(int(statusCode))
|
||||
return output.ErrorEnvelope(int(statusCode), msg, info.suggestion), &APIError{
|
||||
StatusCode: int(statusCode),
|
||||
Code: int(statusCode),
|
||||
Message: msg,
|
||||
Kind: info.kind,
|
||||
Suggestion: info.suggestion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -177,17 +185,43 @@ func (c *Client) Delete(path string, query url.Values) (*output.Envelope, error)
|
|||
return c.Do("DELETE", path, nil, query)
|
||||
}
|
||||
|
||||
func suggestFix(code int) string {
|
||||
switch code {
|
||||
case 401:
|
||||
return "请先运行 gitlink-cli auth login 登录"
|
||||
case 403:
|
||||
return "权限不足,请确认账户权限或联系项目管理员"
|
||||
case 404:
|
||||
return "资源不存在,请检查 owner/repo/id 是否正确"
|
||||
case 422:
|
||||
return "参数校验失败,请检查请求参数"
|
||||
default:
|
||||
return ""
|
||||
type statusInfo struct {
|
||||
kind clierrors.ErrorKind
|
||||
message string
|
||||
suggestion string
|
||||
}
|
||||
|
||||
var statusMessages = map[int]statusInfo{
|
||||
-2: {clierrors.KindAuth, "未登录或 Token 已过期",
|
||||
"运行 gitlink-cli auth login 重新登录,或检查 GITLINK_TOKEN 环境变量"},
|
||||
-1: {clierrors.KindInput, "参数校验失败",
|
||||
"检查必填参数是否缺失、参数格式是否正确,运行 gitlink-cli <命令> --help 查看用法"},
|
||||
0: {clierrors.KindUnknown, "操作失败", ""},
|
||||
// Standard HTTP codes
|
||||
401: {clierrors.KindAuth, "认证失败",
|
||||
"运行 gitlink-cli auth login 登录,或检查 GITLINK_TOKEN 环境变量"},
|
||||
403: {clierrors.KindForbidden, "权限不足",
|
||||
"请确认账号有此仓库的访问权限,或联系项目管理员"},
|
||||
404: {clierrors.KindNotFound, "资源不存在",
|
||||
"检查 owner/repo/id 是否正确,资源可能已被删除"},
|
||||
422: {clierrors.KindInput, "参数校验失败",
|
||||
"检查请求参数格式,运行 gitlink-cli <命令> --help 查看用法"},
|
||||
429: {clierrors.KindServer, "请求过于频繁",
|
||||
"稍等片刻后重试"},
|
||||
500: {clierrors.KindServer, "服务器内部错误",
|
||||
"稍等后重试,如持续出现请联系平台管理员"},
|
||||
502: {clierrors.KindServer, "网关错误",
|
||||
"服务器暂时不可用,稍等后重试"},
|
||||
503: {clierrors.KindServer, "服务暂时不可用",
|
||||
"服务器正在维护,稍等后重试"},
|
||||
}
|
||||
|
||||
func lookupStatusInfo(code int) statusInfo {
|
||||
if info, ok := statusMessages[code]; ok {
|
||||
return info
|
||||
}
|
||||
return statusInfo{
|
||||
kind: clierrors.KindUnknown,
|
||||
message: fmt.Sprintf("API 返回错误码 %d", code),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func ResolveOwnerRepo(flagOwner, flagRepo string) (string, string, error) {
|
|||
owner, repo, err := fromGitRemote()
|
||||
if err != nil {
|
||||
if flagOwner == "" || flagRepo == "" {
|
||||
return "", "", fmt.Errorf("cannot detect owner/repo from git remote: %w\nUse --owner and --repo flags to specify explicitly", err)
|
||||
return "", "", fmt.Errorf("无法自动检测 owner/repo: %w\n 请使用 --owner 和 --repo 参数显式指定,或切换到 git 仓库目录下执行", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,11 +59,33 @@ func parseRemoteURL(remote string) (string, string, error) {
|
|||
}
|
||||
|
||||
func parsePathSegments(path string) (string, string, error) {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
// 处理URL路径格式:可能包含域名前缀
|
||||
// 例如://www.gitlink.org.cn/zzx-coder/gitlink-cli 或 /zzx-coder/gitlink-cli
|
||||
|
||||
// 先去掉域名部分(如果存在)
|
||||
if strings.HasPrefix(path, "//www.gitlink.org.cn/") {
|
||||
path = strings.TrimPrefix(path, "//www.gitlink.org.cn/")
|
||||
} else if strings.HasPrefix(path, "//") {
|
||||
// 处理其他可能的域名格式:找到第二个斜杠后的内容
|
||||
if idx := strings.Index(path[2:], "/"); idx != -1 {
|
||||
path = path[2+idx+1:]
|
||||
} else {
|
||||
path = path[2:]
|
||||
}
|
||||
} else if strings.HasPrefix(path, "/") {
|
||||
// 去掉单个前导斜杠
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
}
|
||||
|
||||
// 去掉.git后缀
|
||||
path = strings.TrimSuffix(path, ".git")
|
||||
parts := strings.SplitN(path, "/", 3)
|
||||
|
||||
// 现在应该得到 "zzx-coder/gitlink-cli" 格式
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("cannot extract owner/repo from path: %s", path)
|
||||
}
|
||||
|
||||
// 第一个部分是owner,第二个是repo(可能还有更多部分但忽略)
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrorKind categorizes errors by user-actionability.
|
||||
type ErrorKind string
|
||||
|
||||
const (
|
||||
KindAuth ErrorKind = "auth" // Login/token issues — user can re-login
|
||||
KindInput ErrorKind = "input" // Parameter issues — user can fix arguments
|
||||
KindConfig ErrorKind = "config" // Config file issues — user can edit config
|
||||
KindNetwork ErrorKind = "network" // Network issues — user can check/retry
|
||||
KindGit ErrorKind = "git" // Git repo issues — user needs correct directory
|
||||
KindServer ErrorKind = "server" // Server-side error — user should wait or contact admin
|
||||
KindNotFound ErrorKind = "not_found" // Resource not found — user can check ID
|
||||
KindForbidden ErrorKind = "forbidden" // Permission denied — user can request access
|
||||
KindUnknown ErrorKind = "unknown" // Unclassified error
|
||||
)
|
||||
|
||||
// CLIError is the unified CLI error type with multi-layered information.
|
||||
type CLIError struct {
|
||||
Kind ErrorKind // Error category for programmatic handling
|
||||
Message string // Human-readable description of what went wrong
|
||||
Detail string // Low-level technical detail (shown in debug mode)
|
||||
Suggestion string // Actionable advice for the user
|
||||
Command string // The command that triggered the error (e.g., "issue +create")
|
||||
Cause error // The underlying error
|
||||
}
|
||||
|
||||
func (e *CLIError) Error() string {
|
||||
var b strings.Builder
|
||||
|
||||
// Header line: kind + command
|
||||
b.WriteString(string(e.Kind))
|
||||
b.WriteString(" error")
|
||||
if e.Command != "" {
|
||||
b.WriteString(" — ")
|
||||
b.WriteString(e.Command)
|
||||
}
|
||||
|
||||
// Body: message
|
||||
if e.Message != "" {
|
||||
b.WriteString("\n\n reason: ")
|
||||
b.WriteString(e.Message)
|
||||
}
|
||||
|
||||
// Suggestion
|
||||
if e.Suggestion != "" {
|
||||
b.WriteString("\n suggestion: ")
|
||||
b.WriteString(e.Suggestion)
|
||||
}
|
||||
|
||||
// Detail (always included in Error() so users see the raw cause)
|
||||
if e.Detail != "" {
|
||||
b.WriteString("\n detail: ")
|
||||
b.WriteString(e.Detail)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (e *CLIError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// New creates a CLIError with the given parameters.
|
||||
func New(kind ErrorKind, message, suggestion string) *CLIError {
|
||||
return &CLIError{
|
||||
Kind: kind,
|
||||
Message: message,
|
||||
Suggestion: suggestion,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap creates a CLIError that wraps an underlying cause.
|
||||
func Wrap(kind ErrorKind, message, suggestion string, cause error) *CLIError {
|
||||
return &CLIError{
|
||||
Kind: kind,
|
||||
Message: message,
|
||||
Suggestion: suggestion,
|
||||
Cause: cause,
|
||||
Detail: cause.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithCommand sets the command context on the error.
|
||||
func (e *CLIError) WithCommand(cmd string) *CLIError {
|
||||
e.Command = cmd
|
||||
return e
|
||||
}
|
||||
|
||||
// InputError is a convenience constructor for parameter errors.
|
||||
func InputError(message, suggestion string) *CLIError {
|
||||
return New(KindInput, message, suggestion)
|
||||
}
|
||||
|
||||
// AuthError is a convenience constructor for authentication errors.
|
||||
func AuthError(message, suggestion string) *CLIError {
|
||||
return New(KindAuth, message, suggestion)
|
||||
}
|
||||
|
||||
// ConfigError creates a config-related error with the config file path in the suggestion.
|
||||
func ConfigError(message string, cause error) *CLIError {
|
||||
return Wrap(KindConfig, message,
|
||||
fmt.Sprintf("检查配置文件 %s 是否正确", configPathPlaceholder()), cause)
|
||||
}
|
||||
|
||||
// configPathPlaceholder avoids circular import; the actual path will be resolved
|
||||
// in output formatting.
|
||||
func configPathPlaceholder() string {
|
||||
return "~/.config/gitlink-cli/config.yaml"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "gitlink-cli",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/branches", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取分支列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -50,7 +50,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name", "--name feature/new-thing")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
from := ctx.Arg("from")
|
||||
if from == "" {
|
||||
from = "master"
|
||||
|
|
@ -61,7 +64,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建分支失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -81,13 +84,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name", "--name feature/new-thing")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"branch_name": name,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches/delete", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除分支失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -107,13 +113,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name", "--name feature/new-thing")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"branch_name": name,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/protected_branches", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("设置分支保护失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -133,11 +142,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/protected_branches/%s", ctx.RepoPath(), url.PathEscape(name)), nil)
|
||||
name, err := ctx.RequireArg("name", "--name feature/new-thing")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/protected_branches/%s", ctx.RepoPath(), url.PathEscape(name)), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("取消分支保护失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/builds", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 CI 构建列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -42,7 +42,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
build, _ := ctx.RequireArg("build")
|
||||
build, err := ctx.RequireArg("build", "--build 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stage := ctx.Arg("stage")
|
||||
step := ctx.Arg("step")
|
||||
if stage == "" {
|
||||
|
|
@ -53,7 +56,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/builds/%s/logs/%s/%s", ctx.RepoPath(), build, stage, step), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取构建日志失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -73,11 +76,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
build, _ := ctx.RequireArg("build")
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/builds/%s/restart", ctx.RepoPath(), build), nil)
|
||||
build, err := ctx.RequireArg("build", "--build 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/builds/%s/restart", ctx.RepoPath(), build), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("重启构建失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -96,11 +102,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
build, _ := ctx.RequireArg("build")
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/builds/%s/stop", ctx.RepoPath(), build), nil)
|
||||
build, err := ctx.RequireArg("build", "--build 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/builds/%s/stop", ctx.RepoPath(), build), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("停止构建失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ func MountShortcut(parent *cobra.Command, s *Shortcut) {
|
|||
}
|
||||
}
|
||||
|
||||
ctx, err := NewRuntimeContext(flagValues)
|
||||
commandName := parent.Use + " +" + s.Name
|
||||
ctx, err := NewRuntimeContext(flagValues, commandName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -59,20 +60,21 @@ func MountShortcut(parent *cobra.Command, s *Shortcut) {
|
|||
}
|
||||
|
||||
for _, f := range s.Flags {
|
||||
usage := f.Usage
|
||||
if f.Required {
|
||||
usage = usage + " [required]"
|
||||
}
|
||||
if f.Bool {
|
||||
defaultValue, _ := strconv.ParseBool(f.Default)
|
||||
if f.Short != "" {
|
||||
cmd.Flags().BoolP(f.Name, f.Short, defaultValue, f.Usage)
|
||||
cmd.Flags().BoolP(f.Name, f.Short, defaultValue, usage)
|
||||
} else {
|
||||
cmd.Flags().Bool(f.Name, defaultValue, f.Usage)
|
||||
cmd.Flags().Bool(f.Name, defaultValue, usage)
|
||||
}
|
||||
} else if f.Short != "" {
|
||||
cmd.Flags().StringP(f.Name, f.Short, f.Default, f.Usage)
|
||||
cmd.Flags().StringP(f.Name, f.Short, f.Default, usage)
|
||||
} else {
|
||||
cmd.Flags().String(f.Name, f.Default, f.Usage)
|
||||
}
|
||||
if f.Required {
|
||||
cmd.MarkFlagRequired(f.Name)
|
||||
cmd.Flags().String(f.Name, f.Default, usage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
|
|
@ -36,15 +37,16 @@ type Flag struct {
|
|||
|
||||
// RuntimeContext provides helpers for shortcut implementations.
|
||||
type RuntimeContext struct {
|
||||
Client *client.Client
|
||||
Owner string
|
||||
Repo string
|
||||
Format string
|
||||
Args map[string]string
|
||||
Client *client.Client
|
||||
Owner string
|
||||
Repo string
|
||||
Format string
|
||||
CommandName string
|
||||
Args map[string]string
|
||||
}
|
||||
|
||||
// NewRuntimeContext creates a RuntimeContext with auto-resolved owner/repo.
|
||||
func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) {
|
||||
func NewRuntimeContext(args map[string]string, commandName string) (*RuntimeContext, error) {
|
||||
cli, err := client.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -53,15 +55,16 @@ func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) {
|
|||
|
||||
format := cmdutil.Format
|
||||
if format == "" {
|
||||
format = "table"
|
||||
format = "json"
|
||||
}
|
||||
|
||||
return &RuntimeContext{
|
||||
Client: cli,
|
||||
Owner: cmdutil.Owner,
|
||||
Repo: cmdutil.Repo,
|
||||
Format: format,
|
||||
Args: args,
|
||||
Client: cli,
|
||||
Owner: cmdutil.Owner,
|
||||
Repo: cmdutil.Repo,
|
||||
Format: format,
|
||||
CommandName: commandName,
|
||||
Args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -114,11 +117,18 @@ func (ctx *RuntimeContext) Arg(name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// RequireArg returns a flag value or an error if not set.
|
||||
func (ctx *RuntimeContext) RequireArg(name string) (string, error) {
|
||||
// RequireArg returns a flag value or a CLIError if not set.
|
||||
func (ctx *RuntimeContext) RequireArg(name, example string) (string, error) {
|
||||
v := ctx.Arg(name)
|
||||
if v == "" {
|
||||
return "", fmt.Errorf("required flag --%s is missing", name)
|
||||
suggestion := fmt.Sprintf("请提供 --%s 参数", name)
|
||||
if example != "" {
|
||||
suggestion += fmt.Sprintf(",例如:%s", example)
|
||||
}
|
||||
return "", clierrors.InputError(
|
||||
fmt.Sprintf("required flag --%s is missing", name),
|
||||
suggestion,
|
||||
).WithCommand(ctx.CommandName)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,42 @@ const (
|
|||
statusRejected = 6
|
||||
)
|
||||
|
||||
// Tracker constants
|
||||
const (
|
||||
trackerBug = 1
|
||||
trackerFeature = 2
|
||||
trackerSupport = 3
|
||||
trackerDoc = 4
|
||||
trackerTest = 5
|
||||
trackerDuplicate = 6
|
||||
trackerQuestion = 7
|
||||
)
|
||||
|
||||
var priorityNames = map[int]string{
|
||||
priorityLow: "low",
|
||||
priorityNormal: "normal",
|
||||
priorityHigh: "high",
|
||||
priorityUrgent: "urgent",
|
||||
}
|
||||
|
||||
var statusNames = map[int]string{
|
||||
statusNew: "new",
|
||||
statusInProgress: "in-progress",
|
||||
statusResolved: "resolved",
|
||||
statusClosed: "closed",
|
||||
statusRejected: "rejected",
|
||||
}
|
||||
|
||||
var trackerNames = map[int]string{
|
||||
trackerBug: "bug",
|
||||
trackerFeature: "feature",
|
||||
trackerSupport: "support",
|
||||
trackerDoc: "doc",
|
||||
trackerTest: "test",
|
||||
trackerDuplicate: "duplicate",
|
||||
trackerQuestion: "question",
|
||||
}
|
||||
|
||||
// Tag name → GitLink tag ID mapping
|
||||
// Collect IDs from web UI DevTools: change tag → capture PATCH payload → get issue_tag_ids value
|
||||
var tagIDs = map[string]int{
|
||||
|
|
@ -42,6 +78,15 @@ var tagIDs = map[string]int{
|
|||
"搁置": 315532,
|
||||
}
|
||||
|
||||
// labelNames returns all known tag names from the given mapping.
|
||||
func labelNames(tags map[string]int) string {
|
||||
var names []string
|
||||
for name := range tags {
|
||||
names = append(names, name)
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// BatchResult is a single item result in a batch operation.
|
||||
type BatchResult struct {
|
||||
Number string `json:"number" yaml:"number"`
|
||||
|
|
@ -327,8 +372,7 @@ func runBatchAssign(ctx *common.RuntimeContext) error {
|
|||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
body := map[string]interface{}{"assigner_ids": []interface{}{assigneeID}}
|
||||
if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil {
|
||||
if err := updateIssueField(ctx, number, map[string]interface{}{"assigned_to_id": assigneeID}); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
|
|
@ -369,7 +413,7 @@ func runBatchLabel(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
label := ctx.Arg("label")
|
||||
trackerID, err := parseLabel(label)
|
||||
trackerID, err := parseTracker(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -400,7 +444,7 @@ func runBatchLabel(ctx *common.RuntimeContext) error {
|
|||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
if err := updateIssueField(ctx, number, map[string]interface{}{"issue_tag_ids": []interface{}{trackerID}}); err != nil {
|
||||
if err := updateIssueField(ctx, number, map[string]interface{}{"tracker_id": trackerID}); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
|
|
@ -507,23 +551,40 @@ func parsePriority(p string) (int, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func parseLabel(name string) (int, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if id, ok := tagIDs[name]; ok && id != 0 {
|
||||
func parseTracker(label string) (int, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(label)) {
|
||||
case "bug":
|
||||
return trackerBug, nil
|
||||
case "feature":
|
||||
return trackerFeature, nil
|
||||
case "support":
|
||||
return trackerSupport, nil
|
||||
case "doc":
|
||||
return trackerDoc, nil
|
||||
case "test":
|
||||
return trackerTest, nil
|
||||
case "duplicate":
|
||||
return trackerDuplicate, nil
|
||||
case "question":
|
||||
return trackerQuestion, nil
|
||||
default:
|
||||
if id, err := strconv.Atoi(label); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
return 0, fmt.Errorf("invalid label %q: use bug, feature, support, doc, test, duplicate, or question", label)
|
||||
}
|
||||
}
|
||||
|
||||
// parseLabel converts a label name to its GitLink tag ID.
|
||||
// tags is the project's name→id mapping from resolveIssueTags.
|
||||
func parseLabel(name string, tags map[string]int) (int, error) {
|
||||
if id, ok := tags[name]; ok && id != 0 {
|
||||
return id, nil
|
||||
}
|
||||
if id, err := strconv.Atoi(name); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
return 0, fmt.Errorf("label %q not found or tag ID not configured; valid names: %s", name, labelNames())
|
||||
}
|
||||
|
||||
func labelNames() string {
|
||||
names := make([]string, 0, len(tagIDs))
|
||||
for n := range tagIDs {
|
||||
names = append(names, n)
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
return 0, fmt.Errorf("invalid label %q: not found in project issue tags", name)
|
||||
}
|
||||
|
||||
func collectIssueNumbers(numbersValue, csvPath string) ([]string, error) {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,76 @@ package issue
|
|||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var issueTagCache sync.Map
|
||||
|
||||
// resolveIssueTags fetches the project's issue tags and returns a name→id mapping.
|
||||
// Results are cached per owner/repo.
|
||||
func resolveIssueTags(ctx *common.RuntimeContext) (map[string]int, error) {
|
||||
key := ctx.Owner + "/" + ctx.Repo
|
||||
if cached, ok := issueTagCache.Load(key); ok {
|
||||
return cached.(map[string]int), nil
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/v1/%s/%s/issue_tags", ctx.Owner, ctx.Repo)
|
||||
q := url.Values{}
|
||||
q.Set("only_name", "true")
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取项目标签列表失败: %w", err)
|
||||
}
|
||||
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("标签列表响应格式异常")
|
||||
}
|
||||
|
||||
rawTags, ok := data["issue_tags"].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("标签列表响应缺少 issue_tags 字段")
|
||||
}
|
||||
|
||||
tags := make(map[string]int, len(rawTags))
|
||||
for _, item := range rawTags {
|
||||
tag, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := tag["name"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
var id int
|
||||
switch v := tag["id"].(type) {
|
||||
case float64:
|
||||
id = int(v)
|
||||
case int:
|
||||
id = v
|
||||
default:
|
||||
id, _ = strconv.Atoi(fmt.Sprintf("%v", v))
|
||||
}
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
tags[name] = id
|
||||
}
|
||||
|
||||
if len(tags) == 0 {
|
||||
return nil, fmt.Errorf("项目没有配置任何标签,请先在 GitLink 网页端创建标签")
|
||||
}
|
||||
|
||||
issueTagCache.Store(key, tags)
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func newBatchCreateShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-create",
|
||||
|
|
@ -49,6 +113,11 @@ func runBatchCreate(ctx *common.RuntimeContext) error {
|
|||
return err
|
||||
}
|
||||
|
||||
tags, err := resolveIssueTags(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
template := strings.ToLower(strings.TrimSpace(ctx.Arg("template")))
|
||||
|
||||
|
|
@ -99,7 +168,7 @@ func runBatchCreate(ctx *common.RuntimeContext) error {
|
|||
continue
|
||||
}
|
||||
|
||||
body := buildCreateBody(ctx, input, template)
|
||||
body := buildCreateBody(ctx, input, template, tags)
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body)
|
||||
if err != nil {
|
||||
result.Status = "failed"
|
||||
|
|
@ -126,7 +195,7 @@ func runBatchCreate(ctx *common.RuntimeContext) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, template string) map[string]interface{} {
|
||||
func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, template string, tags map[string]int) map[string]interface{} {
|
||||
statusID := statusNew
|
||||
if input.Status != "" {
|
||||
if sid, err := parseStatus(input.Status); err == nil {
|
||||
|
|
@ -143,9 +212,9 @@ func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, templat
|
|||
if template != "" {
|
||||
body["description"] = buildTemplateDescription(input, template)
|
||||
if template == "bug" {
|
||||
body["issue_tag_ids"] = []interface{}{tagIDs["缺陷"]}
|
||||
body["issue_tag_ids"] = []interface{}{tags["缺陷"]}
|
||||
} else if template == "feature" {
|
||||
body["issue_tag_ids"] = []interface{}{tagIDs["功能"]}
|
||||
body["issue_tag_ids"] = []interface{}{tags["功能"]}
|
||||
}
|
||||
} else if input.Body != "" {
|
||||
body["description"] = input.Body
|
||||
|
|
@ -157,7 +226,7 @@ func buildCreateBody(ctx *common.RuntimeContext, input createIssueInput, templat
|
|||
}
|
||||
}
|
||||
if input.Label != "" {
|
||||
if tid, err := parseLabel(input.Label); err == nil {
|
||||
if tid, err := parseLabel(input.Label, tags); err == nil {
|
||||
body["issue_tag_ids"] = []interface{}{tid}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,678 @@
|
|||
package issue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// testTags is a static name→id mapping used by unit tests.
|
||||
var testTags = map[string]int{
|
||||
"缺陷": 315526,
|
||||
"功能": 315527,
|
||||
"文档": 315533,
|
||||
"任务": 315530,
|
||||
"测试": 315534,
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
func findShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
t.Helper()
|
||||
for _, s := range Shortcuts() {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
t.Fatalf("shortcut %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockTagsHandler returns a handler that responds to the issue_tags API.
|
||||
func mockTagsHandler(t *testing.T) http.HandlerFunc {
|
||||
t.Helper()
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tags := make([]map[string]interface{}, 0, len(testTags))
|
||||
for name, id := range testTags {
|
||||
tags = append(tags, map[string]interface{}{
|
||||
"id": float64(id),
|
||||
"name": name,
|
||||
})
|
||||
}
|
||||
writeJSONResp(t, w, map[string]interface{}{"issue_tags": tags})
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchCreateShortcut(t *testing.T, server *httptest.Server, args map[string]string) error {
|
||||
t.Helper()
|
||||
s := findShortcut(t, "batch-create")
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
return s.Run(ctx)
|
||||
}
|
||||
|
||||
func writeJSONResp(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func decodeReqBody(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
// ---- runBatchCreate tests ----
|
||||
|
||||
func TestBatchCreate_DryRun(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
|
||||
mockTagsHandler(t)(w, r)
|
||||
return
|
||||
}
|
||||
t.Fatalf("no API calls expected in dry-run mode, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"titles": "标题1,标题2",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_FromTitles(t *testing.T) {
|
||||
var createdBodies []map[string]interface{}
|
||||
callCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"):
|
||||
mockTagsHandler(t)(w, r)
|
||||
case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"):
|
||||
callCount++
|
||||
body := decodeReqBody(t, r)
|
||||
createdBodies = append(createdBodies, body)
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"project_issues_index": float64(100 + callCount),
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"titles": "Bug修复,功能开发",
|
||||
"state": "new",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if callCount != 2 {
|
||||
t.Fatalf("expected 2 API calls, got %d", callCount)
|
||||
}
|
||||
if createdBodies[0]["subject"] != "Bug修复" {
|
||||
t.Fatalf("first title: got %q, want %q", createdBodies[0]["subject"], "Bug修复")
|
||||
}
|
||||
if createdBodies[1]["subject"] != "功能开发" {
|
||||
t.Fatalf("second title: got %q, want %q", createdBodies[1]["subject"], "功能开发")
|
||||
}
|
||||
// Verify required fields — values come through JSON as float64
|
||||
for i, body := range createdBodies {
|
||||
if body["done_ratio"] != float64(0) {
|
||||
t.Fatalf("body[%d]: done_ratio = %v (type %T), want 0", i, body["done_ratio"], body["done_ratio"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_NoTitles(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
|
||||
mockTagsHandler(t)(w, r)
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_FromCSV(t *testing.T) {
|
||||
csvPath := writeTempCSV(t, "title,priority,label,status\nCSV标题1,high,缺陷,new\nCSV标题2,normal,功能,new\n")
|
||||
|
||||
var created []map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"):
|
||||
mockTagsHandler(t)(w, r)
|
||||
case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"):
|
||||
created = append(created, decodeReqBody(t, r))
|
||||
writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"from": csvPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(created) != 2 {
|
||||
t.Fatalf("expected 2 creates, got %d", len(created))
|
||||
}
|
||||
if created[0]["subject"] != "CSV标题1" {
|
||||
t.Fatalf("first subject: got %q", created[0]["subject"])
|
||||
}
|
||||
if created[1]["subject"] != "CSV标题2" {
|
||||
t.Fatalf("second subject: got %q", created[1]["subject"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_CSVMissingTitleColumn(t *testing.T) {
|
||||
csvPath := writeTempCSV(t, "name,description\nval1,desc1\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
|
||||
mockTagsHandler(t)(w, r)
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{"from": csvPath})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing title column, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_CSVOnlyHeader(t *testing.T) {
|
||||
csvPath := writeTempCSV(t, "title,description\n")
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags") {
|
||||
mockTagsHandler(t)(w, r)
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{"from": csvPath})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for header-only CSV, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_PartialFailure(t *testing.T) {
|
||||
callCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && strings.Contains(r.URL.Path, "/issue_tags"):
|
||||
mockTagsHandler(t)(w, r)
|
||||
case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/v1/owner/repo/issues"):
|
||||
callCount++
|
||||
if callCount == 2 {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSONResp(t, w, map[string]interface{}{"project_issues_index": float64(callCount)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"titles": "ok1,fail1,ok2",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from partial failure, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to create") {
|
||||
t.Fatalf("error should mention failed count, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- buildCreateBody tests (direct call, values retain Go types) ----
|
||||
|
||||
func intVal(v interface{}) int {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n
|
||||
case float64:
|
||||
return int(n)
|
||||
}
|
||||
return -999
|
||||
}
|
||||
|
||||
func TestBuildCreateBody_Basic(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
|
||||
input := createIssueInput{Title: "Test issue", Status: "new"}
|
||||
body := buildCreateBody(ctx, input, "", testTags)
|
||||
if body["subject"] != "Test issue" {
|
||||
t.Fatalf("subject: got %v", body["subject"])
|
||||
}
|
||||
if intVal(body["done_ratio"]) != 0 {
|
||||
t.Fatalf("done_ratio: got %v (%T), want 0", body["done_ratio"], body["done_ratio"])
|
||||
}
|
||||
if intVal(body["status_id"]) != 1 {
|
||||
t.Fatalf("status_id: got %v (%T), want 1", body["status_id"], body["status_id"])
|
||||
}
|
||||
if intVal(body["priority_id"]) != 2 {
|
||||
t.Fatalf("priority_id: got %v (%T), want 2", body["priority_id"], body["priority_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCreateBody_BugTemplate(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
|
||||
input := createIssueInput{
|
||||
Title: "登录报错",
|
||||
Version: "v2.0",
|
||||
Severity: "严重",
|
||||
Steps: "1. 打开页面\n2. 点击登录",
|
||||
Expected: "正常登录",
|
||||
Actual: "报错 500",
|
||||
}
|
||||
body := buildCreateBody(ctx, input, "bug", testTags)
|
||||
if body["subject"] != "登录报错" {
|
||||
t.Fatalf("subject: got %v", body["subject"])
|
||||
}
|
||||
desc, _ := body["description"].(string)
|
||||
if !strings.Contains(desc, "## Bug 描述") {
|
||||
t.Fatal("bug description missing header")
|
||||
}
|
||||
if !strings.Contains(desc, "v2.0") {
|
||||
t.Fatal("bug description missing version")
|
||||
}
|
||||
if !strings.Contains(desc, "严重") {
|
||||
t.Fatal("bug description missing severity")
|
||||
}
|
||||
if rawTags, ok := body["issue_tag_ids"]; !ok {
|
||||
t.Fatal("bug template missing issue_tag_ids")
|
||||
} else {
|
||||
ids := rawTags.([]interface{})
|
||||
if intVal(ids[0]) != testTags["缺陷"] {
|
||||
t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], testTags["缺陷"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCreateBody_FeatureTemplate(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
|
||||
input := createIssueInput{
|
||||
Title: "用户搜索",
|
||||
UserStory: "作为用户,我想搜索内容",
|
||||
Acceptance: "搜索结果正确显示",
|
||||
}
|
||||
body := buildCreateBody(ctx, input, "feature", testTags)
|
||||
desc, _ := body["description"].(string)
|
||||
if !strings.Contains(desc, "## 用户故事") {
|
||||
t.Fatal("feature description missing user story header")
|
||||
}
|
||||
if !strings.Contains(desc, "作为用户") {
|
||||
t.Fatal("feature description missing user story content")
|
||||
}
|
||||
if !strings.Contains(desc, "## 验收标准") {
|
||||
t.Fatal("feature description missing acceptance criteria")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCreateBody_WithPriorityLabel(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
|
||||
input := createIssueInput{
|
||||
Title: "紧急修复",
|
||||
Priority: "high",
|
||||
Label: "缺陷",
|
||||
}
|
||||
body := buildCreateBody(ctx, input, "", testTags)
|
||||
if intVal(body["priority_id"]) != 3 {
|
||||
t.Fatalf("priority_id: got %v (type %T), want 3 (high)", body["priority_id"], body["priority_id"])
|
||||
}
|
||||
if rawTags, ok := body["issue_tag_ids"]; ok {
|
||||
ids := rawTags.([]interface{})
|
||||
if intVal(ids[0]) != testTags["缺陷"] {
|
||||
t.Fatalf("tag: got %v (type %T), want %v", ids[0], ids[0], testTags["缺陷"])
|
||||
}
|
||||
} else {
|
||||
t.Fatal("missing issue_tag_ids")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- buildBugDescription tests ----
|
||||
|
||||
func TestBuildBugDescription_AllFields(t *testing.T) {
|
||||
input := createIssueInput{
|
||||
Title: "登录报错",
|
||||
Version: "v2.0",
|
||||
Severity: "严重",
|
||||
Steps: "1. 打开",
|
||||
Expected: "正常",
|
||||
Actual: "500错误",
|
||||
}
|
||||
result := buildBugDescription(input)
|
||||
if !strings.Contains(result, "## Bug 描述") {
|
||||
t.Fatal("missing Bug 描述")
|
||||
}
|
||||
if !strings.Contains(result, "登录报错") {
|
||||
t.Fatal("missing title")
|
||||
}
|
||||
if !strings.Contains(result, "## 版本") {
|
||||
t.Fatal("missing 版本")
|
||||
}
|
||||
if !strings.Contains(result, "## 严重程度") {
|
||||
t.Fatal("missing 严重程度")
|
||||
}
|
||||
if !strings.Contains(result, "## 复现步骤") {
|
||||
t.Fatal("missing 复现步骤")
|
||||
}
|
||||
if !strings.Contains(result, "## 期望结果") {
|
||||
t.Fatal("missing 期望结果")
|
||||
}
|
||||
if !strings.Contains(result, "## 实际结果") {
|
||||
t.Fatal("missing 实际结果")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBugDescription_PartialFields(t *testing.T) {
|
||||
input := createIssueInput{Title: "小问题"}
|
||||
result := buildBugDescription(input)
|
||||
if !strings.Contains(result, "## Bug 描述") {
|
||||
t.Fatal("missing header")
|
||||
}
|
||||
if strings.Contains(result, "## 版本") {
|
||||
t.Fatal("should not have version section")
|
||||
}
|
||||
if strings.Contains(result, "## 严重程度") {
|
||||
t.Fatal("should not have severity section")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- buildFeatureDescription tests ----
|
||||
|
||||
func TestBuildFeatureDescription_AllFields(t *testing.T) {
|
||||
input := createIssueInput{
|
||||
Title: "搜索功能",
|
||||
UserStory: "作为用户想搜索",
|
||||
Body: "详细描述",
|
||||
Acceptance: "搜索结果正确",
|
||||
Priority: "high",
|
||||
}
|
||||
result := buildFeatureDescription(input)
|
||||
if !strings.Contains(result, "## 用户故事") {
|
||||
t.Fatal("missing user story")
|
||||
}
|
||||
if !strings.Contains(result, "作为用户想搜索") {
|
||||
t.Fatal("missing user story content")
|
||||
}
|
||||
if !strings.Contains(result, "## 描述") {
|
||||
t.Fatal("missing description")
|
||||
}
|
||||
if !strings.Contains(result, "## 验收标准") {
|
||||
t.Fatal("missing acceptance criteria")
|
||||
}
|
||||
if !strings.Contains(result, "## 优先级") {
|
||||
t.Fatal("missing priority")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFeatureDescription_FallbackToTitleAsUserStory(t *testing.T) {
|
||||
input := createIssueInput{Title: "搜索功能"}
|
||||
result := buildFeatureDescription(input)
|
||||
if !strings.Contains(result, "搜索功能") {
|
||||
t.Fatal("should fall back to title as user story")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- readCreateInputsFromCSV tests ----
|
||||
|
||||
func TestReadCreateInputsFromCSV_Normal(t *testing.T) {
|
||||
path := writeTempCSV(t, "title,priority,label,status,version,severity,steps,expected,actual\n标题1,high,缺陷,new,v1,严重,,,\n标题2,normal,功能,new,,,,,\n")
|
||||
|
||||
inputs, err := readCreateInputsFromCSV(path, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(inputs) != 2 {
|
||||
t.Fatalf("got %d inputs, want 2", len(inputs))
|
||||
}
|
||||
if inputs[0].Title != "标题1" {
|
||||
t.Fatalf("first title: got %q", inputs[0].Title)
|
||||
}
|
||||
if inputs[0].Severity != "严重" {
|
||||
t.Fatalf("severity: got %q", inputs[0].Severity)
|
||||
}
|
||||
if inputs[1].Label != "功能" {
|
||||
t.Fatalf("label: got %q", inputs[1].Label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCreateInputsFromCSV_MissingTitleColumn(t *testing.T) {
|
||||
path := writeTempCSV(t, "name,description\nval1,desc1\n")
|
||||
_, err := readCreateInputsFromCSV(path, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCreateInputsFromCSV_OnlyHeader(t *testing.T) {
|
||||
path := writeTempCSV(t, "title,priority\n")
|
||||
_, err := readCreateInputsFromCSV(path, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCreateInputsFromCSV_SkipsEmptyTitle(t *testing.T) {
|
||||
path := writeTempCSV(t, "title,priority\n标题1,high\n,normal\n标题2,low\n")
|
||||
inputs, err := readCreateInputsFromCSV(path, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(inputs) != 2 {
|
||||
t.Fatalf("got %d inputs, want 2 (empty row skipped)", len(inputs))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- parseTitles tests ----
|
||||
|
||||
func TestParseTitles_CommaSeparated(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{
|
||||
Owner: "o", Repo: "r",
|
||||
Args: map[string]string{"priority": "normal"},
|
||||
}
|
||||
inputs := parseTitles("标题1, 标题2, , 标题3", ctx)
|
||||
if len(inputs) != 3 {
|
||||
t.Fatalf("got %d inputs, want 3", len(inputs))
|
||||
}
|
||||
if inputs[0].Title != "标题1" {
|
||||
t.Fatalf("got %q", inputs[0].Title)
|
||||
}
|
||||
if inputs[2].Title != "标题3" {
|
||||
t.Fatalf("got %q", inputs[2].Title)
|
||||
}
|
||||
if inputs[0].Priority != "normal" {
|
||||
t.Fatalf("priority not propagated: got %q", inputs[0].Priority)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- normalizeHeader tests ----
|
||||
|
||||
func TestNormalizeHeader(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"Title", "title"},
|
||||
{" PRIORITY ", "priority"},
|
||||
{"user_story", "user_story"},
|
||||
{"User Story", "user story"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := normalizeHeader(c.in)
|
||||
if got != c.want {
|
||||
t.Fatalf("normalizeHeader(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- getCol tests ----
|
||||
|
||||
func TestGetCol(t *testing.T) {
|
||||
col := map[string]int{"title": 0, "priority": 1}
|
||||
record := []string{"测试标题", "high"}
|
||||
if got := getCol(record, col, "title"); got != "测试标题" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
if got := getCol(record, col, "missing"); got != "" {
|
||||
t.Fatalf("got %q, want empty", got)
|
||||
}
|
||||
if got := getCol(record, col, "priority"); got != "high" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- truncate tests ----
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
if got := truncate("short", 40); got != "short" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
long := "这是一个很长的标题用来测试截断功能一二三四五六七八九十"
|
||||
got := truncate(long, 10)
|
||||
if len([]rune(got)) > 13 {
|
||||
t.Fatalf("truncated too long: %q (%d runes)", got, len([]rune(got)))
|
||||
}
|
||||
if !strings.HasSuffix(got, "...") {
|
||||
t.Fatal("truncated string should end with ...")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- priority/label/status parse helpers ----
|
||||
|
||||
func TestParsePriorityStrings(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want int
|
||||
}{
|
||||
{"low", 1}, {"normal", 2}, {"high", 3}, {"urgent", 4},
|
||||
{"LOW", 1}, {"High", 3},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := parsePriority(c.in)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePriority(%q): %v", c.in, err)
|
||||
}
|
||||
if got != c.want {
|
||||
t.Fatalf("parsePriority(%q) = %d, want %d", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePriorityNumeric(t *testing.T) {
|
||||
got, err := parsePriority("5")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != 5 {
|
||||
t.Fatalf("got %d, want 5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePriorityInvalid(t *testing.T) {
|
||||
if _, err := parsePriority("invalid"); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLabelValid(t *testing.T) {
|
||||
id, err := parseLabel("缺陷", testTags)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if id != testTags["缺陷"] {
|
||||
t.Fatalf("got %d, want %d", id, testTags["缺陷"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLabelInvalid(t *testing.T) {
|
||||
if _, err := parseLabel("不存在的标签", testTags); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLabelNumeric(t *testing.T) {
|
||||
id, err := parseLabel("999", testTags)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if id != 999 {
|
||||
t.Fatalf("got %d, want 999", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelNamesReturnsAll(t *testing.T) {
|
||||
names := labelNames(testTags)
|
||||
if !strings.Contains(names, "缺陷") {
|
||||
t.Fatal("missing 缺陷 in label names")
|
||||
}
|
||||
if !strings.Contains(names, "功能") {
|
||||
t.Fatal("missing 功能 in label names")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- buildCreateBody status tests ----
|
||||
|
||||
func TestBuildCreateBody_DefaultStatus(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
|
||||
input := createIssueInput{Title: "t", Status: ""}
|
||||
body := buildCreateBody(ctx, input, "", testTags)
|
||||
if intVal(body["status_id"]) != 1 {
|
||||
t.Fatalf("default status_id: got %v (type %T), want 1", body["status_id"], body["status_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCreateBody_ClosedStatus(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "o", Repo: "r", Args: map[string]string{}}
|
||||
input := createIssueInput{Title: "t", Status: "closed"}
|
||||
body := buildCreateBody(ctx, input, "", testTags)
|
||||
if intVal(body["status_id"]) != 5 {
|
||||
t.Fatalf("closed status_id: got %v (type %T), want 5", body["status_id"], body["status_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- regression ----
|
||||
|
||||
func TestCollectIssueNumbers_FromBatchCreatePerspective(t *testing.T) {
|
||||
got, err := collectIssueNumbers("1,2,3", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := []string{"1", "2", "3"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Issue 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -74,7 +74,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
title, err := ctx.RequireArg("title", `--title "Bug: 登录页崩溃"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -110,13 +110,13 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := ctx.RequireArg("number", "--number 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -136,7 +136,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := ctx.RequireArg("number", "--number 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("关闭 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -206,7 +206,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := ctx.RequireArg("number", "--number 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -241,7 +241,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新 Issue 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -262,11 +262,11 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := ctx.RequireArg("number", "--number 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ctx.RequireArg("body")
|
||||
body, err := ctx.RequireArg("body", `--body "可以这样复现..."`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -275,7 +275,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("添加 Issue 评论失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -286,7 +286,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
func fetchExistingIssue(ctx *common.RuntimeContext, number string) (*existingIssue, error) {
|
||||
getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("获取 Issue 信息失败: %w", err)
|
||||
}
|
||||
issueData, ok := getEnv.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/organizations", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取组织列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -36,11 +36,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "id", Short: "i", Usage: "Organization ID or login", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/organizations/%s", id), nil)
|
||||
id, err := ctx.RequireArg("id", "--id my-org")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/organizations/%s", id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看组织失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -53,13 +56,16 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
id, _ := ctx.RequireArg("id")
|
||||
id, err := ctx.RequireArg("id", "--id my-org")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/organizations/%s/organization_users", id), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取组织成员失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -77,7 +83,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "description", Short: "d", Usage: "Description"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
name, _ := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name", `--name "My Organization"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"name": name,
|
||||
}
|
||||
|
|
@ -86,7 +95,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", "/organizations", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建组织失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/pulls", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 PR 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -61,8 +61,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, _ := ctx.RequireArg("title")
|
||||
head, _ := ctx.RequireArg("head")
|
||||
title, err := ctx.RequireArg("title", `--title "Fix login crash"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
head, err := ctx.RequireArg("head", `--head feat/new-login`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := ctx.Arg("base")
|
||||
if base == "" {
|
||||
base = "master"
|
||||
|
|
@ -77,7 +83,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/pulls", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -92,11 +98,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id", "--id 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -120,7 +129,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
id, err := ctx.RequireArg("id", "--id 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
method := ctx.Arg("method")
|
||||
if method == "" {
|
||||
method = "merge"
|
||||
|
|
@ -130,7 +142,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/pr_merge", ctx.RepoPath(), id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("合并 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -150,11 +162,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/refuse_merge", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id", "--id 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/pulls/%s/refuse_merge", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("关闭 PR 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -168,11 +183,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id", "--id 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 PR 文件列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -186,11 +204,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id", "--id 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/files", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 PR diff 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -210,12 +231,18 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
body, _ := ctx.RequireArg("body")
|
||||
id, err := ctx.RequireArg("id", "--id 42")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := ctx.RequireArg("body", `--body "Looks good to me"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch PR: %w", err)
|
||||
return fmt.Errorf("获取 PR 信息失败: %w", err)
|
||||
}
|
||||
issueID, err := extractIssueID(prEnv)
|
||||
if err != nil {
|
||||
|
|
@ -227,7 +254,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/%s/issues/%d/journals", ctx.Owner, ctx.Repo, issueID), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("添加 PR 评论失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/releases", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Release 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -51,8 +51,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
tag, _ := ctx.RequireArg("tag")
|
||||
name, _ := ctx.RequireArg("name")
|
||||
tag, err := ctx.RequireArg("tag", "--tag v1.0.0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name", `--name "Version 1.0.0"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"tag_name": tag,
|
||||
"name": name,
|
||||
|
|
@ -68,7 +74,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/releases", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Release 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -83,11 +89,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
id, err := ctx.RequireArg("id", "--id 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看 Release 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
|
|
@ -106,7 +115,10 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := ctx.RequireArg("id")
|
||||
id, err := ctx.RequireArg("id", "--id 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, delErr := ctx.CallAPI("DELETE", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
if delErr != nil {
|
||||
_, viewErr := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
||||
|
|
@ -115,7 +127,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
"message": "删除成功",
|
||||
}, nil))
|
||||
}
|
||||
return delErr
|
||||
return fmt.Errorf("删除 Release 失败: %w", delErr)
|
||||
}
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "删除成功",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
type repoCreateInput struct {
|
||||
Name string
|
||||
Description string
|
||||
Private bool
|
||||
}
|
||||
|
||||
type repoBatchResult struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
type repoBatchSummary struct {
|
||||
Owner string `json:"owner" yaml:"owner"`
|
||||
Action string `json:"action" yaml:"action"`
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Succeeded int `json:"succeeded" yaml:"succeeded"`
|
||||
Failed int `json:"failed" yaml:"failed"`
|
||||
Results []repoBatchResult `json:"results" yaml:"results"`
|
||||
}
|
||||
|
||||
func newBatchCreateShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-create",
|
||||
Description: "Create multiple repositories from CLI flags or a CSV file",
|
||||
Flags: []common.Flag{
|
||||
{Name: "names", Short: "n", Usage: "Comma-separated repository names, e.g. repo-a,repo-b"},
|
||||
{Name: "from", Usage: "CSV file path"},
|
||||
{Name: "description", Short: "d", Usage: "Shared description for all repos (inline mode)"},
|
||||
{Name: "private", Usage: "Make repos private", Bool: true, Default: "false"},
|
||||
{Name: "dry-run", Usage: "Preview without creating", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchCreate,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchCreate(ctx *common.RuntimeContext) error {
|
||||
var inputs []repoCreateInput
|
||||
|
||||
if namesStr := ctx.Arg("names"); namesStr != "" {
|
||||
for _, name := range strings.Split(namesStr, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
inputs = append(inputs, repoCreateInput{
|
||||
Name: name,
|
||||
Description: ctx.Arg("description"),
|
||||
Private: ctx.Arg("private") == "true",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if csvPath := ctx.Arg("from"); csvPath != "" {
|
||||
csvInputs, err := readRepoInputsFromCSV(csvPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
inputs = append(inputs, csvInputs...)
|
||||
}
|
||||
|
||||
if len(inputs) == 0 {
|
||||
return fmt.Errorf("no repository names provided; use -n repo-a,repo-b or --from repos.csv")
|
||||
}
|
||||
|
||||
dryRun := ctx.Arg("dry-run") == "true"
|
||||
|
||||
var login string
|
||||
var userID int
|
||||
if !dryRun {
|
||||
userEnv, err := ctx.CallAPI("GET", "/users/me", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current user: %w", err)
|
||||
}
|
||||
userData, _ := userEnv.Data.(map[string]interface{})
|
||||
login, _ = userData["login"].(string)
|
||||
if login == "" {
|
||||
return fmt.Errorf("cannot determine current user login")
|
||||
}
|
||||
if uid, ok := userData["user_id"].(float64); ok {
|
||||
userID = int(uid)
|
||||
}
|
||||
}
|
||||
|
||||
summary := repoBatchSummary{
|
||||
Owner: login,
|
||||
Action: "create",
|
||||
DryRun: dryRun,
|
||||
Total: len(inputs),
|
||||
Results: make([]repoBatchResult, 0, len(inputs)),
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
result := repoBatchResult{Name: input.Name}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": input.Name,
|
||||
"repository_name": input.Name,
|
||||
"user_id": userID,
|
||||
}
|
||||
if input.Description != "" {
|
||||
body["description"] = input.Description
|
||||
}
|
||||
if input.Private {
|
||||
body["private"] = true
|
||||
}
|
||||
|
||||
if _, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s", login, input.Name), body); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
} else {
|
||||
result.Status = "created"
|
||||
summary.Succeeded++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
if err := ctx.OutputData(summary); err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d repo(s) failed to create", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRepoInputsFromCSV(path string) ([]repoCreateInput, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CSV: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse CSV: %w", err)
|
||||
}
|
||||
if len(records) < 2 {
|
||||
return nil, fmt.Errorf("CSV must have a header row and at least one data row")
|
||||
}
|
||||
|
||||
header := records[0]
|
||||
col := make(map[string]int)
|
||||
for i, h := range header {
|
||||
col[strings.ToLower(strings.TrimSpace(h))] = i
|
||||
}
|
||||
if _, ok := col["name"]; !ok {
|
||||
return nil, fmt.Errorf("CSV must have a 'name' column")
|
||||
}
|
||||
|
||||
var inputs []repoCreateInput
|
||||
for _, record := range records[1:] {
|
||||
name := getCol(record, col, "name")
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
private := false
|
||||
if p := strings.ToLower(getCol(record, col, "private")); p == "true" || p == "1" {
|
||||
private = true
|
||||
}
|
||||
inputs = append(inputs, repoCreateInput{
|
||||
Name: name,
|
||||
Description: getCol(record, col, "description"),
|
||||
Private: private,
|
||||
})
|
||||
}
|
||||
return inputs, nil
|
||||
}
|
||||
|
||||
func getCol(record []string, col map[string]int, name string) string {
|
||||
if idx, ok := col[name]; ok && idx < len(record) {
|
||||
return strings.TrimSpace(record[idx])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -0,0 +1,837 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
func findShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
t.Helper()
|
||||
for _, s := range Shortcuts() {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
t.Fatalf("shortcut %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runBatchCreateShortcut(t *testing.T, server *httptest.Server, args map[string]string) error {
|
||||
t.Helper()
|
||||
s := findShortcut(t, "batch-create")
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
return s.Run(ctx)
|
||||
}
|
||||
|
||||
func runBatchUpdateShortcut(t *testing.T, server *httptest.Server, args map[string]string) error {
|
||||
t.Helper()
|
||||
s := findShortcut(t, "batch-update")
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
return s.Run(ctx)
|
||||
}
|
||||
|
||||
func writeJSONResp(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
t.Fatalf("writeJSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeReqBody(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeTempCSV(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "repos.csv")
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatalf("write temp csv: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// ---- runBatchCreate tests ----
|
||||
|
||||
func TestBatchCreate_DryRun(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("no API calls expected in dry-run mode, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "repo1,repo2",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_FromNames(t *testing.T) {
|
||||
var createdBodies []map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/users/me.json":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"login": "testuser",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.Method == "POST" && strings.HasPrefix(r.URL.Path, "/testuser/"):
|
||||
createdBodies = append(createdBodies, decodeReqBody(t, r))
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "repo-a,repo-b",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(createdBodies) != 2 {
|
||||
t.Fatalf("expected 2 API calls, got %d", len(createdBodies))
|
||||
}
|
||||
if createdBodies[0]["name"] != "repo-a" {
|
||||
t.Fatalf("first name: got %q, want %q", createdBodies[0]["name"], "repo-a")
|
||||
}
|
||||
if createdBodies[1]["name"] != "repo-b" {
|
||||
t.Fatalf("second name: got %q, want %q", createdBodies[1]["name"], "repo-b")
|
||||
}
|
||||
for _, body := range createdBodies {
|
||||
if body["repository_name"] != body["name"] {
|
||||
t.Fatalf("repository_name should match name: %v vs %v", body["repository_name"], body["name"])
|
||||
}
|
||||
if body["user_id"] != float64(42) {
|
||||
t.Fatalf("user_id: got %v, want 42", body["user_id"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_NoNames(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_NoNamesDryRun(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{"dry-run": "true"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for no names even in dry-run, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_WithPrivate(t *testing.T) {
|
||||
var createdBody map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/users/me.json":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"login": "testuser",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.Method == "POST":
|
||||
createdBody = decodeReqBody(t, r)
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "private-repo",
|
||||
"private": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if createdBody["private"] != true {
|
||||
t.Fatalf("private: got %v, want true", createdBody["private"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_WithDescription(t *testing.T) {
|
||||
var createdBody map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/users/me.json":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"login": "testuser",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.Method == "POST":
|
||||
createdBody = decodeReqBody(t, r)
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "desc-repo",
|
||||
"description": "shared description",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if createdBody["description"] != "shared description" {
|
||||
t.Fatalf("description: got %q, want %q", createdBody["description"], "shared description")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_FromCSV(t *testing.T) {
|
||||
csvPath := writeTempCSV(t, "name,description,private\ncsv-repo1,desc one,false\ncsv-repo2,desc two,true\n")
|
||||
|
||||
var createdBodies []map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/users/me.json":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"login": "testuser",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.Method == "POST":
|
||||
createdBodies = append(createdBodies, decodeReqBody(t, r))
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{"from": csvPath})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(createdBodies) != 2 {
|
||||
t.Fatalf("expected 2 creates, got %d", len(createdBodies))
|
||||
}
|
||||
if createdBodies[0]["name"] != "csv-repo1" {
|
||||
t.Fatalf("first name: got %q", createdBodies[0]["name"])
|
||||
}
|
||||
if createdBodies[0]["description"] != "desc one" {
|
||||
t.Fatalf("first description: got %q", createdBodies[0]["description"])
|
||||
}
|
||||
if createdBodies[1]["name"] != "csv-repo2" {
|
||||
t.Fatalf("second name: got %q", createdBodies[1]["name"])
|
||||
}
|
||||
if createdBodies[1]["private"] != true {
|
||||
t.Fatalf("second private: got %v, want true", createdBodies[1]["private"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_PartialFailure(t *testing.T) {
|
||||
callCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/users/me.json":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"login": "testuser",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.Method == "POST":
|
||||
callCount++
|
||||
if callCount == 2 {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(callCount)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "ok1,fail1,ok2",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from partial failure, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to create") {
|
||||
t.Fatalf("error should mention failed count, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_UserLookupFails(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "repo1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to get current user") {
|
||||
t.Fatalf("error should mention user lookup, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_UserMissingLogin(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"user_id": float64(42),
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "repo1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot determine current user login") {
|
||||
t.Fatalf("error should mention missing login, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreate_CSVAndNamesCombined(t *testing.T) {
|
||||
csvPath := writeTempCSV(t, "name\ndual-repo\n")
|
||||
|
||||
var createdNames []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/users/me.json":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"login": "testuser",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.Method == "POST":
|
||||
body := decodeReqBody(t, r)
|
||||
createdNames = append(createdNames, body["name"].(string))
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchCreateShortcut(t, server, map[string]string{
|
||||
"names": "inline-repo",
|
||||
"from": csvPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(createdNames) != 2 {
|
||||
t.Fatalf("expected 2 creates, got %d", len(createdNames))
|
||||
}
|
||||
if createdNames[0] != "inline-repo" {
|
||||
t.Fatalf("first: got %q", createdNames[0])
|
||||
}
|
||||
if createdNames[1] != "dual-repo" {
|
||||
t.Fatalf("second: got %q", createdNames[1])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- readRepoInputsFromCSV tests ----
|
||||
|
||||
func TestReadRepoInputsFromCSV_Normal(t *testing.T) {
|
||||
path := writeTempCSV(t, "name,description,private\nrepo1,desc1,true\nrepo2,desc2,false\nrepo3,,0\n")
|
||||
|
||||
inputs, err := readRepoInputsFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(inputs) != 3 {
|
||||
t.Fatalf("got %d inputs, want 3", len(inputs))
|
||||
}
|
||||
if inputs[0].Name != "repo1" || inputs[0].Description != "desc1" || !inputs[0].Private {
|
||||
t.Fatalf("input[0]: %+v", inputs[0])
|
||||
}
|
||||
if inputs[1].Name != "repo2" || inputs[1].Private {
|
||||
t.Fatalf("input[1]: %+v", inputs[1])
|
||||
}
|
||||
if inputs[2].Name != "repo3" || inputs[2].Private {
|
||||
t.Fatalf("input[2]: %+v", inputs[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRepoInputsFromCSV_MissingNameColumn(t *testing.T) {
|
||||
path := writeTempCSV(t, "title,description\nval1,desc1\n")
|
||||
_, err := readRepoInputsFromCSV(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRepoInputsFromCSV_OnlyHeader(t *testing.T) {
|
||||
path := writeTempCSV(t, "name,description\n")
|
||||
_, err := readRepoInputsFromCSV(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRepoInputsFromCSV_SkipsEmptyName(t *testing.T) {
|
||||
path := writeTempCSV(t, "name,description\nrepo1,desc1\n,desc2\nrepo2,desc3\n")
|
||||
|
||||
inputs, err := readRepoInputsFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(inputs) != 2 {
|
||||
t.Fatalf("got %d inputs, want 2", len(inputs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRepoInputsFromCSV_PrivateParsing(t *testing.T) {
|
||||
path := writeTempCSV(t, "name,private\nr1,true\nr2,false\nr3,1\nr4,0\nr5,\n")
|
||||
|
||||
inputs, err := readRepoInputsFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(inputs) != 5 {
|
||||
t.Fatalf("got %d inputs, want 5", len(inputs))
|
||||
}
|
||||
if !inputs[0].Private {
|
||||
t.Fatal("r1 (true) should be private")
|
||||
}
|
||||
if inputs[1].Private {
|
||||
t.Fatal("r2 (false) should not be private")
|
||||
}
|
||||
if !inputs[2].Private {
|
||||
t.Fatal("r3 (1) should be private")
|
||||
}
|
||||
if inputs[3].Private {
|
||||
t.Fatal("r4 (0) should not be private")
|
||||
}
|
||||
if inputs[4].Private {
|
||||
t.Fatal("r5 (empty) should not be private")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRepoInputsFromCSV_FileNotFound(t *testing.T) {
|
||||
_, err := readRepoInputsFromCSV("/nonexistent/path.csv")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRepoInputsFromCSV_CaseInsensitiveHeader(t *testing.T) {
|
||||
path := writeTempCSV(t, "NAME,Description,Private\nrepo1,desc1,TRUE\n")
|
||||
|
||||
inputs, err := readRepoInputsFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(inputs) != 1 {
|
||||
t.Fatalf("got %d inputs, want 1", len(inputs))
|
||||
}
|
||||
if inputs[0].Name != "repo1" {
|
||||
t.Fatalf("name: got %q", inputs[0].Name)
|
||||
}
|
||||
if !inputs[0].Private {
|
||||
t.Fatal("should be private")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- getCol tests ----
|
||||
|
||||
func TestGetCol_Found(t *testing.T) {
|
||||
col := map[string]int{"name": 0, "description": 1}
|
||||
record := []string{"my-repo", "my desc"}
|
||||
if got := getCol(record, col, "name"); got != "my-repo" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
if got := getCol(record, col, "description"); got != "my desc" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCol_Missing(t *testing.T) {
|
||||
col := map[string]int{"name": 0}
|
||||
record := []string{"my-repo"}
|
||||
if got := getCol(record, col, "missing"); got != "" {
|
||||
t.Fatalf("got %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCol_IndexOutOfRange(t *testing.T) {
|
||||
col := map[string]int{"name": 5}
|
||||
record := []string{"my-repo"}
|
||||
if got := getCol(record, col, "name"); got != "" {
|
||||
t.Fatalf("got %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- runBatchUpdate tests ----
|
||||
|
||||
func TestBatchUpdate_DryRun(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("no API calls expected in dry-run mode, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "repo1,repo2",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_FromNames(t *testing.T) {
|
||||
var fetchedRepos []string
|
||||
var patchedBodies []map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/owner/"), ".json")
|
||||
fetchedRepos = append(fetchedRepos, name)
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"name": name,
|
||||
"identifier": "ident-" + name,
|
||||
})
|
||||
case r.Method == "PATCH":
|
||||
patchedBodies = append(patchedBodies, decodeReqBody(t, r))
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "repo-a,repo-b",
|
||||
"private": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(fetchedRepos) != 2 {
|
||||
t.Fatalf("expected 2 fetches, got %d", len(fetchedRepos))
|
||||
}
|
||||
if len(patchedBodies) != 2 {
|
||||
t.Fatalf("expected 2 patches, got %d", len(patchedBodies))
|
||||
}
|
||||
for _, body := range patchedBodies {
|
||||
if body["private"] != true {
|
||||
t.Fatalf("private should be true: %+v", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_NoNames(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_PrivatePublicConflict(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "repo1",
|
||||
"private": "true",
|
||||
"public": "true",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot use both --private and --public") {
|
||||
t.Fatalf("error should mention conflict, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_SetPublic(t *testing.T) {
|
||||
var patchedBody map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"name": "repo1",
|
||||
"identifier": "abc123",
|
||||
})
|
||||
case r.Method == "PATCH":
|
||||
patchedBody = decodeReqBody(t, r)
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "repo1",
|
||||
"public": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if patchedBody["private"] != false {
|
||||
t.Fatalf("public should set private=false, got %v", patchedBody["private"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_WithDescription(t *testing.T) {
|
||||
var patchedBody map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"name": "repo1",
|
||||
"identifier": "abc123",
|
||||
})
|
||||
case r.Method == "PATCH":
|
||||
patchedBody = decodeReqBody(t, r)
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "repo1",
|
||||
"description": "updated description",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if patchedBody["description"] != "updated description" {
|
||||
t.Fatalf("description: got %q", patchedBody["description"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_FromCSV(t *testing.T) {
|
||||
csvPath := writeTempCSV(t, "name\ncsv-repo1\ncsv-repo2\n")
|
||||
|
||||
var fetchedRepos []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
name := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/owner/"), ".json")
|
||||
fetchedRepos = append(fetchedRepos, name)
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"name": name,
|
||||
"identifier": "ident-" + name,
|
||||
})
|
||||
case r.Method == "PATCH":
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{"from": csvPath})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(fetchedRepos) != 2 {
|
||||
t.Fatalf("expected 2 fetches, got %d", len(fetchedRepos))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_PartialFailure(t *testing.T) {
|
||||
callCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
switch {
|
||||
case r.Method == "GET" && strings.Contains(r.URL.Path, "fail-repo"):
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case r.Method == "GET":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"name": "ok-repo",
|
||||
"identifier": "abc123",
|
||||
})
|
||||
case r.Method == "PATCH":
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "ok-repo,fail-repo",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from partial failure, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to update") {
|
||||
t.Fatalf("error should mention failed count, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdate_PreservesIdentifier(t *testing.T) {
|
||||
var patchedBody map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
writeJSONResp(t, w, map[string]interface{}{
|
||||
"name": "my-repo",
|
||||
"identifier": "xyz-789",
|
||||
"description": "old desc",
|
||||
})
|
||||
case r.Method == "PATCH":
|
||||
patchedBody = decodeReqBody(t, r)
|
||||
writeJSONResp(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runBatchUpdateShortcut(t, server, map[string]string{
|
||||
"names": "my-repo",
|
||||
"description": "new desc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if patchedBody["name"] != "my-repo" {
|
||||
t.Fatalf("name: got %q", patchedBody["name"])
|
||||
}
|
||||
if patchedBody["identifier"] != "xyz-789" {
|
||||
t.Fatalf("identifier: got %q", patchedBody["identifier"])
|
||||
}
|
||||
if patchedBody["description"] != "new desc" {
|
||||
t.Fatalf("description: got %q", patchedBody["description"])
|
||||
}
|
||||
}
|
||||
|
||||
// ---- readNamesFromCSV tests ----
|
||||
|
||||
func TestReadNamesFromCSV_Normal(t *testing.T) {
|
||||
path := writeTempCSV(t, "name\nrepo1\nrepo2\nrepo3\n")
|
||||
|
||||
names, err := readNamesFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(names) != 3 {
|
||||
t.Fatalf("got %d names, want 3", len(names))
|
||||
}
|
||||
if names[0] != "repo1" || names[1] != "repo2" || names[2] != "repo3" {
|
||||
t.Fatalf("got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNamesFromCSV_MissingNameColumn(t *testing.T) {
|
||||
path := writeTempCSV(t, "title,description\nval1,desc1\nval2,desc2\n")
|
||||
|
||||
names, err := readNamesFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("got %d names, want 2 (falls back to col 0)", len(names))
|
||||
}
|
||||
if names[0] != "val1" || names[1] != "val2" {
|
||||
t.Fatalf("got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNamesFromCSV_OnlyHeader(t *testing.T) {
|
||||
path := writeTempCSV(t, "name\n")
|
||||
_, err := readNamesFromCSV(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNamesFromCSV_SkipsEmptyName(t *testing.T) {
|
||||
path := writeTempCSV(t, "name\nrepo1\n\nrepo2\n")
|
||||
|
||||
names, err := readNamesFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("got %d names, want 2", len(names))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNamesFromCSV_FileNotFound(t *testing.T) {
|
||||
_, err := readNamesFromCSV("/nonexistent/path.csv")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNamesFromCSV_CaseInsensitiveHeader(t *testing.T) {
|
||||
path := writeTempCSV(t, "NAME\nrepo1\nrepo2\n")
|
||||
|
||||
names, err := readNamesFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("got %d names, want 2", len(names))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadNamesFromCSV_ExtraColumns(t *testing.T) {
|
||||
path := writeTempCSV(t, "name,extra,another\nrepo1,x,y\nrepo2,a,b\n")
|
||||
|
||||
names, err := readNamesFromCSV(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("got %d names, want 2", len(names))
|
||||
}
|
||||
if names[0] != "repo1" || names[1] != "repo2" {
|
||||
t.Fatalf("got %v", names)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func newBatchUpdateShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-update",
|
||||
Description: "Update settings for multiple repositories",
|
||||
Flags: []common.Flag{
|
||||
{Name: "names", Short: "n", Usage: "Comma-separated repository names, e.g. repo-a,repo-b"},
|
||||
{Name: "from", Usage: "CSV file path"},
|
||||
{Name: "description", Short: "d", Usage: "Shared description for all repos"},
|
||||
{Name: "private", Usage: "Set repos to private", Bool: true, Default: "false"},
|
||||
{Name: "public", Usage: "Set repos to public", Bool: true, Default: "false"},
|
||||
{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchUpdate(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var repoNames []string
|
||||
if namesStr := ctx.Arg("names"); namesStr != "" {
|
||||
for _, name := range strings.Split(namesStr, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
repoNames = append(repoNames, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if csvPath := ctx.Arg("from"); csvPath != "" {
|
||||
csvNames, err := readNamesFromCSV(csvPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repoNames = append(repoNames, csvNames...)
|
||||
}
|
||||
if len(repoNames) == 0 {
|
||||
return fmt.Errorf("no repository names provided; use -n repo-a,repo-b or --from repos.csv")
|
||||
}
|
||||
|
||||
setPrivate := ctx.Arg("private") == "true"
|
||||
setPublic := ctx.Arg("public") == "true"
|
||||
if setPrivate && setPublic {
|
||||
return fmt.Errorf("cannot use both --private and --public")
|
||||
}
|
||||
changeVisibility := setPrivate || setPublic
|
||||
|
||||
dryRun := ctx.Arg("dry-run") == "true"
|
||||
desc := ctx.Arg("description")
|
||||
|
||||
summary := repoBatchSummary{
|
||||
Owner: ctx.Owner,
|
||||
Action: "update",
|
||||
DryRun: dryRun,
|
||||
Total: len(repoNames),
|
||||
Results: make([]repoBatchResult, 0, len(repoNames)),
|
||||
}
|
||||
|
||||
for _, name := range repoNames {
|
||||
result := repoBatchResult{Name: name}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
// Fetch current repo info to get required fields for PATCH
|
||||
current, err := ctx.CallAPI("GET", fmt.Sprintf("/%s/%s", ctx.Owner, name), nil)
|
||||
if err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = fmt.Sprintf("fetch repo: %v", err)
|
||||
summary.Failed++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
curData, _ := current.Data.(map[string]interface{})
|
||||
curName, _ := curData["name"].(string)
|
||||
identifier, _ := curData["identifier"].(string)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": curName,
|
||||
"identifier": identifier,
|
||||
}
|
||||
if desc != "" {
|
||||
body["description"] = desc
|
||||
}
|
||||
if changeVisibility {
|
||||
body["private"] = setPrivate
|
||||
}
|
||||
|
||||
if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("/%s/%s", ctx.Owner, name), body); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
} else {
|
||||
result.Status = "updated"
|
||||
summary.Succeeded++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
if err := ctx.OutputData(summary); err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d repo(s) failed to update", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readNamesFromCSV(path string) ([]string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CSV: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
reader.TrimLeadingSpace = true
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse CSV: %w", err)
|
||||
}
|
||||
if len(records) < 2 {
|
||||
return nil, fmt.Errorf("CSV must have a header row and at least one data row")
|
||||
}
|
||||
|
||||
header := records[0]
|
||||
nameCol := -1
|
||||
for i, h := range header {
|
||||
if strings.ToLower(strings.TrimSpace(h)) == "name" {
|
||||
nameCol = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if nameCol == -1 {
|
||||
nameCol = 0
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, record := range records[1:] {
|
||||
if nameCol >= len(record) {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(record[nameCol])
|
||||
if name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
|
@ -9,7 +9,9 @@ import (
|
|||
)
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
shortcuts := []*common.Shortcut{
|
||||
shortcuts := []*common.Shortcut{
|
||||
newBatchCreateShortcut(),
|
||||
newBatchUpdateShortcut(),
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List repositories for a user or organization",
|
||||
|
|
@ -34,7 +36,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取仓库列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -48,7 +50,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -67,14 +69,14 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "private", Usage: "Make repository private (true/false)", Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
name, err := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name", `--name "my-project"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Get current user login for the create path
|
||||
userEnv, err := ctx.CallAPI("GET", "/users/me", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current user: %w", err)
|
||||
return fmt.Errorf("获取当前用户信息失败: %w", err)
|
||||
}
|
||||
userData, _ := userEnv.Data.(map[string]interface{})
|
||||
login, _ := userData["login"].(string)
|
||||
|
|
@ -95,7 +97,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s", login, name), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -116,7 +118,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/forks", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Fork 仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -137,7 +139,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
env, err := ctx.CallAPI("DELETE", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("删除仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,14 +18,17 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
keyword, _ := ctx.RequireArg("keyword")
|
||||
keyword, err := ctx.RequireArg("keyword", "--keyword my-project")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("search", keyword)
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/projects", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("搜索仓库失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -39,14 +42,17 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
keyword, _ := ctx.RequireArg("keyword")
|
||||
keyword, err := ctx.RequireArg("keyword", "--keyword zhangsan")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("search", keyword)
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/users/list", q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("搜索用户失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
Run: func(ctx *common.RuntimeContext) error {
|
||||
env, err := ctx.CallAPI("GET", "/users/me", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取当前用户信息失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -26,13 +26,13 @@ func Shortcuts() []*common.Shortcut {
|
|||
{Name: "login", Short: "l", Usage: "User login name", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
login, err := ctx.RequireArg("login")
|
||||
login, err := ctx.RequireArg("login", "--login zhangsan")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s", login), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看用户信息失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,330 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// 支持的Webhook事件类型
|
||||
var supportedEvents = []string{
|
||||
"push",
|
||||
"pull_request",
|
||||
"issue",
|
||||
"issue_assign",
|
||||
"issue_comment",
|
||||
"pull_request_assign",
|
||||
"pull_request_comment",
|
||||
"merge_request",
|
||||
"repository",
|
||||
"branch",
|
||||
"tag",
|
||||
}
|
||||
|
||||
func isEventSupported(event string) bool {
|
||||
for _, supported := range supportedEvents {
|
||||
if event == supported {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseEvents(eventsStr string) []string {
|
||||
if eventsStr == "" {
|
||||
return []string{"push"} // 默认事件
|
||||
}
|
||||
events := strings.Split(eventsStr, ",")
|
||||
var validEvents []string
|
||||
for _, event := range events {
|
||||
event = strings.TrimSpace(event)
|
||||
if isEventSupported(event) {
|
||||
validEvents = append(validEvents, event)
|
||||
}
|
||||
}
|
||||
return validEvents
|
||||
}
|
||||
|
||||
// webhookRepoPath returns the webhook API path prefix: /v1/{owner}/{repo}
|
||||
// Note: BaseURL already includes /api prefix
|
||||
func webhookRepoPath(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List all webhooks for a repository",
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", webhookRepoPath(ctx)+"/webhooks", q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 Webhook 列表失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: "Create a new webhook",
|
||||
Flags: []common.Flag{
|
||||
{Name: "url", Short: "u", Usage: "Webhook callback URL", Required: true},
|
||||
{Name: "events", Short: "e", Usage: "Trigger events (comma-separated), e.g., push,pull_request,issue", Default: "push"},
|
||||
{Name: "active", Usage: "Webhook active status (true/false)", Default: "true"},
|
||||
{Name: "secret", Usage: "Webhook secret for HMAC verification"},
|
||||
{Name: "description", Short: "d", Usage: "Webhook description"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webhookURL, err := ctx.RequireArg("url", "--url https://example.com/hook")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
events := parseEvents(ctx.Arg("events"))
|
||||
if len(events) == 0 {
|
||||
return fmt.Errorf("no valid events specified. Supported events: %s", strings.Join(supportedEvents, ", "))
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"url": webhookURL,
|
||||
"http_method": "POST",
|
||||
"active": true,
|
||||
"content_type": "json",
|
||||
}
|
||||
|
||||
if len(events) > 0 {
|
||||
payload["events"] = events
|
||||
} else {
|
||||
payload["events"] = []string{"push"}
|
||||
}
|
||||
|
||||
if secret := ctx.Arg("secret"); secret != "" {
|
||||
payload["secret"] = secret
|
||||
}
|
||||
|
||||
if description := ctx.Arg("description"); description != "" {
|
||||
payload["description"] = description
|
||||
}
|
||||
|
||||
env, err := ctx.CallAPI("POST", webhookRepoPath(ctx)+"/webhooks", payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Description: "Update an existing webhook",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
{Name: "url", Short: "u", Usage: "Webhook callback URL"},
|
||||
{Name: "events", Short: "e", Usage: "Trigger events (comma-separated)"},
|
||||
{Name: "active", Usage: "Webhook active status (true/false)"},
|
||||
{Name: "content_type", Usage: "Content type (json/form)"},
|
||||
{Name: "secret", Usage: "Webhook secret for HMAC verification"},
|
||||
{Name: "description", Short: "d", Usage: "Webhook description"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webhookID, err := ctx.RequireArg("id", "--id 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"http_method": "POST",
|
||||
"active": true,
|
||||
"content_type": "json",
|
||||
}
|
||||
|
||||
// 如果用户没有提供URL,获取当前webhook的URL
|
||||
webhookURL := ctx.Arg("url")
|
||||
if webhookURL == "" {
|
||||
getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 Webhook 当前信息失败: %w", err)
|
||||
}
|
||||
webhookData, ok := getEnv.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to parse webhook data")
|
||||
}
|
||||
currentURL, ok := webhookData["url"].(string)
|
||||
if !ok || currentURL == "" {
|
||||
return fmt.Errorf("failed to get current webhook URL")
|
||||
}
|
||||
webhookURL = currentURL
|
||||
}
|
||||
payload["url"] = webhookURL
|
||||
|
||||
if events := ctx.Arg("events"); events != "" {
|
||||
validEvents := parseEvents(events)
|
||||
if len(validEvents) == 0 {
|
||||
return fmt.Errorf("no valid events specified. Supported events: %s", strings.Join(supportedEvents, ", "))
|
||||
}
|
||||
payload["events"] = validEvents
|
||||
}
|
||||
|
||||
|
||||
if contentType := ctx.Arg("content_type"); contentType != "" {
|
||||
payload["content_type"] = contentType
|
||||
}
|
||||
|
||||
if secret := ctx.Arg("secret"); secret != "" {
|
||||
payload["secret"] = secret
|
||||
}
|
||||
|
||||
if description := ctx.Arg("description"); description != "" {
|
||||
payload["description"] = description
|
||||
}
|
||||
|
||||
|
||||
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete a webhook",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webhookID, err := ctx.RequireArg("id", "--id 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, delErr := ctx.CallAPI("DELETE", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
|
||||
if delErr != nil {
|
||||
// 验证是否真的删除成功(类似release的处理)
|
||||
_, viewErr := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
|
||||
if viewErr != nil {
|
||||
// Webhook不存在了,说明删除成功
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "Webhook deleted successfully",
|
||||
}, nil))
|
||||
}
|
||||
return fmt.Errorf("删除 Webhook 失败: %w", delErr)
|
||||
}
|
||||
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
||||
"message": "Webhook deleted successfully",
|
||||
}, nil))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test",
|
||||
Description: "Test a webhook delivery (send a ping event)",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
{Name: "event", Short: "e", Usage: "Event type to test (default: push)", Default: "push"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webhookID, err := ctx.RequireArg("id", "--id 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventType := ctx.Arg("event")
|
||||
if !isEventSupported(eventType) {
|
||||
return fmt.Errorf("unsupported event type: %s. Supported events: %s", eventType, strings.Join(supportedEvents, ", "))
|
||||
}
|
||||
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", webhookRepoPath(ctx), webhookID), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("测试 Webhook 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "info",
|
||||
Description: "Show webhook details",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webhookID, err := ctx.RequireArg("id", "--id 1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看 Webhook 详情失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "events",
|
||||
Description: "List all supported event types for webhooks",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
eventInfo := make([]map[string]interface{}, 0)
|
||||
for _, event := range supportedEvents {
|
||||
eventInfo = append(eventInfo, map[string]interface{}{
|
||||
"event": event,
|
||||
"supported": true,
|
||||
"description": getEventDescription(event),
|
||||
})
|
||||
}
|
||||
return ctx.Output(output.SuccessEnvelope(eventInfo, nil))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getEventDescription(event string) string {
|
||||
descriptions := map[string]string{
|
||||
"push": "Code push events",
|
||||
"pull_request": "Pull request events",
|
||||
"issue": "Issue events",
|
||||
"issue_assign": "Issue assignment events",
|
||||
"issue_comment": "Issue comment events",
|
||||
"pull_request_assign": "Pull request assignment events",
|
||||
"pull_request_comment":"Pull request comment events",
|
||||
"merge_request": "Merge request events",
|
||||
"repository": "Repository events",
|
||||
"branch": "Branch creation/deletion events",
|
||||
"tag": "Tag creation/deletion events",
|
||||
}
|
||||
if desc, ok := descriptions[event]; ok {
|
||||
return desc
|
||||
}
|
||||
return "Custom event"
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestIsEventSupported(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
event string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "supported event - push",
|
||||
event: "push",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "supported event - pull_request",
|
||||
event: "pull_request",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "supported event - issue",
|
||||
event: "issue",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported event",
|
||||
event: "unsupported_event",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty event",
|
||||
event: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isEventSupported(tt.event)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isEventSupported(%q) = %v, want %v", tt.event, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEvents(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
events string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "single event",
|
||||
events: "push",
|
||||
expected: []string{"push"},
|
||||
},
|
||||
{
|
||||
name: "multiple events",
|
||||
events: "push,pull_request,issue",
|
||||
expected: []string{"push", "pull_request", "issue"},
|
||||
},
|
||||
{
|
||||
name: "events with spaces",
|
||||
events: "push, pull_request, issue",
|
||||
expected: []string{"push", "pull_request", "issue"},
|
||||
},
|
||||
{
|
||||
name: "mixed valid and invalid events",
|
||||
events: "push,invalid_event,pull_request",
|
||||
expected: []string{"push", "pull_request"},
|
||||
},
|
||||
{
|
||||
name: "empty string - default to push",
|
||||
events: "",
|
||||
expected: []string{"push"},
|
||||
},
|
||||
{
|
||||
name: "all invalid events",
|
||||
events: "invalid1,invalid2",
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseEvents(tt.events)
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("parseEvents(%q) returned %d events, want %d", tt.events, len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
for i, event := range result {
|
||||
if event != tt.expected[i] {
|
||||
t.Errorf("parseEvents(%q)[%d] = %q, want %q", tt.events, i, event, tt.expected[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEventDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
event string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "push event description",
|
||||
event: "push",
|
||||
expected: "Code push events",
|
||||
},
|
||||
{
|
||||
name: "pull_request event description",
|
||||
event: "pull_request",
|
||||
expected: "Pull request events",
|
||||
},
|
||||
{
|
||||
name: "issue event description",
|
||||
event: "issue",
|
||||
expected: "Issue events",
|
||||
},
|
||||
{
|
||||
name: "unknown event description",
|
||||
event: "unknown_event",
|
||||
expected: "Custom event",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getEventDescription(tt.event)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getEventDescription(%q) = %q, want %q", tt.event, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortcuts(t *testing.T) {
|
||||
shortcuts := Shortcuts()
|
||||
|
||||
if len(shortcuts) == 0 {
|
||||
t.Fatal("Shortcuts() returned empty slice")
|
||||
}
|
||||
|
||||
// 验证所有必需的shortcuts都存在
|
||||
expectedShortcuts := []string{
|
||||
"list", "create", "update", "delete", "test", "info", "events",
|
||||
}
|
||||
|
||||
shortcutNames := make(map[string]bool)
|
||||
for _, sc := range shortcuts {
|
||||
shortcutNames[sc.Name] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedShortcuts {
|
||||
if !shortcutNames[expected] {
|
||||
t.Errorf("Missing shortcut: %s", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证每个shortcut的基本属性
|
||||
for _, sc := range shortcuts {
|
||||
if sc.Name == "" {
|
||||
t.Error("Shortcut has empty Name")
|
||||
}
|
||||
if sc.Description == "" {
|
||||
t.Errorf("Shortcut %q has empty Description", sc.Name)
|
||||
}
|
||||
if sc.Run == nil {
|
||||
t.Errorf("Shortcut %q has nil Run function", sc.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebhookEventsList tests the events shortcut to ensure it returns valid event information
|
||||
func TestWebhookEventsList(t *testing.T) {
|
||||
shortcuts := Shortcuts()
|
||||
var eventsShortcut *common.Shortcut
|
||||
for _, sc := range shortcuts {
|
||||
if sc.Name == "events" {
|
||||
eventsShortcut = sc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if eventsShortcut == nil {
|
||||
t.Fatal("Events shortcut not found")
|
||||
}
|
||||
|
||||
// 验证所有支持的事件都有描述
|
||||
for _, event := range supportedEvents {
|
||||
desc := getEventDescription(event)
|
||||
if desc == "" {
|
||||
t.Errorf("Event %q has empty description", event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEventValidationIntegration tests event validation in an integrated manner
|
||||
func TestEventValidationIntegration(t *testing.T) {
|
||||
// 测试所有支持的事件都能被正确识别
|
||||
for _, event := range supportedEvents {
|
||||
if !isEventSupported(event) {
|
||||
t.Errorf("Supported event %q is not recognized by isEventSupported", event)
|
||||
}
|
||||
// 确保描述不为空
|
||||
desc := getEventDescription(event)
|
||||
if desc == "" {
|
||||
t.Errorf("Event %q has empty description", event)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试解析包含所有支持的事件字符串
|
||||
allEvents := strings.Join(supportedEvents, ",")
|
||||
parsed := parseEvents(allEvents)
|
||||
if len(parsed) != len(supportedEvents) {
|
||||
t.Errorf("Parsing all events returned %d results, expected %d", len(parsed), len(supportedEvents))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseEvents benchmarks the event parsing function
|
||||
func BenchmarkParseEvents(b *testing.B) {
|
||||
eventsStr := "push,pull_request,issue,issue_assign,issue_comment,pull_request_assign,pull_request_comment"
|
||||
for i := 0; i < b.N; i++ {
|
||||
parseEvents(eventsStr)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkIsEventSupported benchmarks the event validation function
|
||||
func BenchmarkIsEventSupported(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, event := range supportedEvents {
|
||||
isEventSupported(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ func fetchPageContent(ctx *common.RuntimeContext, projectID, pageName string) (s
|
|||
q.Set("pageName", pageName)
|
||||
env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("获取 Wiki 页面现有内容失败: %w", err)
|
||||
}
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
|
|
@ -454,7 +454,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("projectId", projectID)
|
||||
env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("wikiPages"), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("获取 Wiki 页面列表失败: %w", err)
|
||||
}
|
||||
cleanWikiList(env)
|
||||
return ctx.Output(env)
|
||||
|
|
@ -470,7 +470,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
title, err := ctx.RequireArg("title", `--title "Home Page"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -485,7 +485,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("pageName", title)
|
||||
env, err := callWikiAPIWithQuery(ctx, "GET", wikiPath("getWiki"), q)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("查看 Wiki 页面失败: %w", err)
|
||||
}
|
||||
return outputWithDecodedContent(ctx, env)
|
||||
},
|
||||
|
|
@ -508,7 +508,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
title, err := ctx.RequireArg("title", `--title "Home Page"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -536,7 +536,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
|
||||
env, err := callWikiAPI(ctx, "POST", wikiPath("createWiki"), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("创建 Wiki 页面失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -561,7 +561,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
title, err := ctx.RequireArg("title", `--title "Home Page"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -614,7 +614,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
|
||||
env, err := callWikiAPI(ctx, "PUT", wikiPath("updateWiki"), body)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("更新 Wiki 页面失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
|
|
@ -634,7 +634,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := ctx.RequireArg("title")
|
||||
title, err := ctx.RequireArg("title", `--title "Home Page"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -664,7 +664,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
"message": "Wiki page deleted successfully",
|
||||
})
|
||||
}
|
||||
return delErr
|
||||
return fmt.Errorf("删除 Wiki 页面失败: %w", delErr)
|
||||
}
|
||||
return ctx.OutputData(map[string]string{
|
||||
"message": "Wiki page deleted successfully",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
package wiki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func resetProjectIDCache() {
|
||||
projectIDCache = sync.Map{}
|
||||
}
|
||||
|
||||
func newMockServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
t.Fatalf("writeJSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- unwrapGatewayResponse tests ----
|
||||
|
||||
func TestUnwrapGatewayResponse_Success(t *testing.T) {
|
||||
env := output.SuccessEnvelope(map[string]interface{}{
|
||||
"code": float64(200),
|
||||
"data": map[string]interface{}{"id": float64(1)},
|
||||
}, nil)
|
||||
|
||||
result, err := unwrapGatewayResponse(env)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected map data")
|
||||
}
|
||||
if data["id"] != float64(1) {
|
||||
t.Fatalf("got %v, want 1", data["id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapGatewayResponse_Code201(t *testing.T) {
|
||||
env := output.SuccessEnvelope(map[string]interface{}{
|
||||
"code": float64(201),
|
||||
"data": "ok",
|
||||
}, nil)
|
||||
|
||||
result, err := unwrapGatewayResponse(env)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Data != "ok" {
|
||||
t.Fatalf("got %v, want ok", result.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapGatewayResponse_BusinessError(t *testing.T) {
|
||||
env := output.SuccessEnvelope(map[string]interface{}{
|
||||
"code": float64(500),
|
||||
"msg": "内部错误",
|
||||
}, nil)
|
||||
|
||||
_, err := unwrapGatewayResponse(env)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if err.Error() != "[500] 内部错误" {
|
||||
t.Fatalf("got %q, want %q", err.Error(), "[500] 内部错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapGatewayResponse_NoDataField(t *testing.T) {
|
||||
env := output.SuccessEnvelope(map[string]interface{}{
|
||||
"code": float64(200),
|
||||
}, nil)
|
||||
|
||||
result, err := unwrapGatewayResponse(env)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Should return original envelope since no "data" field to extract
|
||||
if result != env {
|
||||
t.Fatal("expected original envelope when no data field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapGatewayResponse_NonMapData(t *testing.T) {
|
||||
env := output.SuccessEnvelope("plain text", nil)
|
||||
|
||||
result, err := unwrapGatewayResponse(env)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Data != "plain text" {
|
||||
t.Fatalf("got %v, want plain text", result.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- resolveProjectID tests ----
|
||||
|
||||
func TestResolveProjectID_Success(t *testing.T) {
|
||||
resetProjectIDCache()
|
||||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/owner1/repo1/detail.json" {
|
||||
writeJSON(t, w, map[string]interface{}{"project_id": float64(123)})
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner1",
|
||||
Repo: "repo1",
|
||||
}
|
||||
pid, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if pid != "123" {
|
||||
t.Fatalf("got %q, want %q", pid, "123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProjectID_Float64(t *testing.T) {
|
||||
resetProjectIDCache()
|
||||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/owner2/repo2/detail.json" {
|
||||
writeJSON(t, w, map[string]interface{}{"project_id": float64(456)})
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner2",
|
||||
Repo: "repo2",
|
||||
}
|
||||
pid, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if pid != "456" {
|
||||
t.Fatalf("got %q, want %q", pid, "456")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProjectID_CacheHit(t *testing.T) {
|
||||
resetProjectIDCache()
|
||||
callCount := 0
|
||||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
if r.URL.Path == "/owner3/repo3/detail.json" {
|
||||
writeJSON(t, w, map[string]interface{}{"project_id": float64(789)})
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner3",
|
||||
Repo: "repo3",
|
||||
}
|
||||
|
||||
pid1, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("first call: %v", err)
|
||||
}
|
||||
if pid1 != "789" {
|
||||
t.Fatalf("first call: got %q, want %q", pid1, "789")
|
||||
}
|
||||
|
||||
pid2, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("second call: %v", err)
|
||||
}
|
||||
if pid2 != "789" {
|
||||
t.Fatalf("second call: got %q, want %q", pid2, "789")
|
||||
}
|
||||
|
||||
if callCount != 1 {
|
||||
t.Fatalf("API called %d times, want 1 (cache miss)", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProjectID_MissingProjectID(t *testing.T) {
|
||||
resetProjectIDCache()
|
||||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(t, w, map[string]interface{}{"name": "no-project-id"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner4",
|
||||
Repo: "repo4",
|
||||
}
|
||||
_, err := resolveProjectID(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProjectID_APIError(t *testing.T) {
|
||||
resetProjectIDCache()
|
||||
server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner5",
|
||||
Repo: "repo5",
|
||||
}
|
||||
_, err := resolveProjectID(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# branch +create
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
从现有分支或 commit 创建新分支。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 从 master 创建分支
|
||||
gitlink-cli branch +create --name feature/new-feature
|
||||
|
||||
# 从指定分支创建
|
||||
gitlink-cli branch +create --name hotfix/bug-123 --from develop
|
||||
|
||||
# 从指定 commit 创建
|
||||
gitlink-cli branch +create --name feature/x --from abc123def
|
||||
|
||||
# 指定仓库创建分支
|
||||
gitlink-cli branch +create --name feature/x --owner someone --repo myrepo
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | **是** | 新分支名称 |
|
||||
| `--from, -f` | 否 | 源分支或 commit(默认 `master`) |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
POST /v1/{owner}/{repo}/branches
|
||||
Body: { "new_branch_name": name, "old_branch_name": from }
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"name": "feature/new-feature",
|
||||
"commit_id": "abc123...",
|
||||
"commit_message": "Create feature branch",
|
||||
"committed_time": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the branch name and source branch with the user.
|
||||
2. **Execute** `gitlink-cli branch +create --name <name> --from <source>`.
|
||||
3. **Report** the created branch information.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent before executing.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **功能开发**:为新功能创建独立分支
|
||||
- **Bug 修复**:从稳定分支创建 hotfix 分支
|
||||
- **实验性功能**:创建实验分支进行尝试
|
||||
- **版本发布**:为发布版本创建分支
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **命名规范**:使用有意义的分支名,如 `feature/xxx`、`hotfix/xxx`、`release/xxx`
|
||||
- **源分支选择**:通常从 `develop` 或 `master` 创建功能分支
|
||||
- **分支描述**:创建后可以添加描述说明分支用途
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 仓库不存在 | 检查 `--owner` 和 `--repo` 是否正确 |
|
||||
| `409` | 分支已存在 | 使用不同的分支名或删除现有分支 |
|
||||
| `404` | 源分支不存在 | 确认 `--from` 指定的分支或 commit 存在 |
|
||||
|
||||
## Tips
|
||||
|
||||
- 默认从 `master` 分支创建,如需从其他分支创建需明确指定
|
||||
- 分支名支持 `/` 分隔符,便于组织分支结构
|
||||
- 创建后可以立即使用 `gitlink-cli branch +list` 验证
|
||||
|
||||
## References
|
||||
|
||||
- [branch +list](branch-list.md) — 列出分支
|
||||
- [branch +delete](branch-delete.md) — 删除分支
|
||||
- [gitlink-branch](../SKILL.md) — 分支操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# branch +delete
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
删除指定的分支。**此操作不可逆,请谨慎使用。**
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 删除分支
|
||||
gitlink-cli branch +delete --name feature/old-feature
|
||||
|
||||
# 指定仓库删除分支
|
||||
gitlink-cli branch +delete --name feature/old-feature --owner someone --repo myrepo
|
||||
|
||||
# 删除带路径的分支
|
||||
gitlink-cli branch +delete --name feature/my-feature
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | **是** | 要删除的分支名称 |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
POST /v1/{owner}/{repo}/branches/delete
|
||||
Body: { "branch_name": name }
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"message": "Branch deleted successfully",
|
||||
"branch_name": "feature/old-feature"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the user really wants to delete this branch (emphasize this is **irreversible**).
|
||||
2. **Check** if the branch exists using `branch +list` if needed.
|
||||
3. **Execute** `gitlink-cli branch +delete --name <name>`.
|
||||
4. **Report** the deletion result.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Destructive Operation** — confirm user intent before executing. This action **cannot be undone**.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **清理已完成的功能分支**:功能合并后删除功能分支
|
||||
- **清理错误的分支**:删除创建错误或不再需要的分支
|
||||
- **维护分支整洁**:定期清理无用分支保持仓库整洁
|
||||
|
||||
## Warnings
|
||||
|
||||
- ⚠️ **不可逆操作**:删除分支后无法恢复
|
||||
- ⚠️ **受保护分支**:无法删除受保护的分支
|
||||
- ⚠️ **默认分支**:无法删除默认分支(通常是 master)
|
||||
- ⚠️ **未合并更改**:删除包含未合并更改的分支可能导致代码丢失
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **确认合并状态**:删除前确认分支的更改已经合并
|
||||
2. **备份重要更改**:如果有重要更改未合并,先备份或合并
|
||||
3. **沟通确认**:团队协作时先沟通确认再删除
|
||||
4. **使用描述性名称**:避免删除错误的分支
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 分支不存在 | 使用 `branch +list` 确认分支存在 |
|
||||
| `403` | 权限不足 | 确认有删除分支的权限 |
|
||||
| `400` | 受保护分支 | 无法删除受保护的分支 |
|
||||
| `400` | 默认分支 | 无法删除默认分支 |
|
||||
|
||||
## Safety Checks
|
||||
|
||||
建议在删除前执行以下检查:
|
||||
|
||||
```bash
|
||||
# 1. 检查分支是否存在
|
||||
gitlink-cli branch +list | grep branch-name
|
||||
|
||||
# 2. 确认不是保护分支
|
||||
gitlink-cli branch +list --format json | jq '.data.branches[] | select(.name=="branch-name") | .protected'
|
||||
|
||||
# 3. 确认不是默认分支
|
||||
gitlink-cli api GET /{owner}/{repo} | jq '.data.default_branch'
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- 删除前建议使用 `gitlink-cli branch +list` 确认分支名称
|
||||
- 对于重要分支,建议先检查是否有未合并的 PR
|
||||
- 团队协作时,删除公共分支前先通知团队成员
|
||||
|
||||
## References
|
||||
|
||||
- [branch +list](branch-list.md) — 列出分支
|
||||
- [branch +create](branch-create.md) — 创建分支
|
||||
- [branch +protect](branch-protect.md) — 保护分支
|
||||
- [gitlink-branch](../SKILL.md) — 分支操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
# branch +list
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
列出仓库的所有分支,支持分页查询。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 列出当前仓库的分支
|
||||
gitlink-cli branch +list
|
||||
|
||||
# 指定仓库并分页
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --page 1 --limit 10
|
||||
|
||||
# 输出为 JSON
|
||||
gitlink-cli branch +list --format json
|
||||
|
||||
# 输出为 YAML
|
||||
gitlink-cli branch +list --format yaml
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--page, -p` | 否 | 页码(默认 `1`) |
|
||||
| `--limit, -l` | 否 | 每页条数(默认 `20`) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET /v1/{owner}/{repo}/branches?page=1&limit=20
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"branches": [
|
||||
{
|
||||
"name": "master",
|
||||
"commit_id": "abc123...",
|
||||
"commit_message": "Initial commit",
|
||||
"committed_time": "2026-01-01T00:00:00Z",
|
||||
"is_default": true,
|
||||
"protected": false
|
||||
},
|
||||
{
|
||||
"name": "develop",
|
||||
"commit_id": "def456...",
|
||||
"commit_message": "Develop branch",
|
||||
"committed_time": "2026-01-02T00:00:00Z",
|
||||
"is_default": false,
|
||||
"protected": true
|
||||
}
|
||||
],
|
||||
"total_count": 15
|
||||
},
|
||||
"meta": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total_count": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Resolve** owner and repo (from git remote or flags).
|
||||
2. **Execute** `gitlink-cli branch +list`.
|
||||
3. **Display** branches in the requested format.
|
||||
|
||||
> [!NOTE]
|
||||
> This is a **Read Operation** — no confirmation needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **查看可用分支**:在创建 PR 前查看所有分支
|
||||
- **检查分支保护状态**:查看哪些分支被保护
|
||||
- **分支浏览**:探索仓库的分支结构
|
||||
- **自动化脚本**:结合 JSON 格式输出进行批量操作
|
||||
|
||||
## Tips
|
||||
|
||||
- 使用 `--format json` 可以更好地解析分支信息
|
||||
- 分支列表包含保护状态,可以快速识别受保护的分支
|
||||
- 支持分页,适合分支较多的仓库
|
||||
|
||||
## References
|
||||
|
||||
- [branch +create](branch-create.md) — 创建分支
|
||||
- [branch +protect](branch-protect.md) — 保护分支
|
||||
- [gitlink-branch](../SKILL.md) — 分支操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
# branch +protect
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
设置分支保护规则,防止重要分支被意外修改或删除。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 保护分支
|
||||
gitlink-cli branch +protect --name main
|
||||
|
||||
# 保护 master 分支
|
||||
gitlink-cli branch +protect --name master
|
||||
|
||||
# 指定仓库保护分支
|
||||
gitlink-cli branch +protect --name main --owner someone --repo myrepo
|
||||
|
||||
# 保护开发分支
|
||||
gitlink-cli branch +protect --name develop
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | **是** | 要保护的分支名称 |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
POST /{owner}/{repo}/protected_branches
|
||||
Body: { "branch_name": name }
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"branch_name": "main",
|
||||
"protected": true,
|
||||
"message": "Branch protection enabled successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the branch name to protect with the user.
|
||||
2. **Execute** `gitlink-cli branch +protect --name <name>`.
|
||||
3. **Report** the protection result.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent before executing.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **保护主分支**:保护 `main` 或 `master` 分支,防止直接推送
|
||||
- **保护发布分支**:保护 `release` 分支,确保发布版本稳定性
|
||||
- **保护开发分支**:保护 `develop` 分支,维护开发主线稳定
|
||||
- **合规要求**:满足团队管理或合规性要求
|
||||
|
||||
## What Protection Means
|
||||
|
||||
分支保护后,以下操作将被限制:
|
||||
- ✅ **仍可操作**:通过 Pull Request 合并更改
|
||||
- ❌ **受限操作**:直接推送代码
|
||||
- ❌ **受限操作**:强制推送
|
||||
- ❌ **受限操作**:删除分支
|
||||
- ❌ **受限操作**:修改历史
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **保护关键分支**:至少保护 `main` 和 `develop` 分支
|
||||
2. **配合 PR 工作流**:强制通过 PR 进行代码审查
|
||||
3. **定期审查**:定期检查和保护重要的分支
|
||||
4. **团队协作**:团队协商确定保护策略
|
||||
|
||||
## Common Protected Branches
|
||||
|
||||
| 分支名 | 用途 | 建议保护 |
|
||||
|--------|------|----------|
|
||||
| `main` / `master` | 主分支 | ✅ 强烈建议 |
|
||||
| `develop` | 开发分支 | ✅ 建议 |
|
||||
| `release/*` | 发布分支 | ✅ 建议 |
|
||||
| `hotfix/*` | 紧急修复分支 | ⚠️ 可选 |
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 分支不存在 | 使用 `branch +list` 确认分支存在 |
|
||||
| `403` | 权限不足 | 确认有管理权限 |
|
||||
| `409` | 已经被保护 | 分支已经处于保护状态 |
|
||||
|
||||
## Safety Considerations
|
||||
|
||||
- ⚠️ **权限要求**:需要管理员或协作者权限
|
||||
- ⚠️ **团队影响**:保护分支影响整个团队的协作流程
|
||||
- ⚠️ **CI/CD 集成**:确保 CI/CD 流程兼容保护规则
|
||||
|
||||
## Tips
|
||||
|
||||
- 保护前先确认分支名称正确
|
||||
- 可以使用 `branch +list --format json` 查看分支保护状态
|
||||
- 设置保护后,团队成员需要通过 PR 贡献代码
|
||||
- 建议在设置保护前通知团队成员
|
||||
|
||||
## Workflow Example
|
||||
|
||||
典型的分支保护工作流:
|
||||
|
||||
```bash
|
||||
# 1. 查看分支列表
|
||||
gitlink-cli branch +list
|
||||
|
||||
# 2. 确认要保护的分支
|
||||
gitlink-cli branch +list --format json | jq '.data.branches[] | select(.name=="main")'
|
||||
|
||||
# 3. 设置分支保护
|
||||
gitlink-cli branch +protect --name main
|
||||
|
||||
# 4. 验证保护设置
|
||||
gitlink-cli branch +list --format json | jq '.data.branchs[] | select(.name=="main") | .protected'
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [branch +unprotect](branch-unprotect.md) — 移除分支保护
|
||||
- [branch +list](branch-list.md) — 列出分支并查看保护状态
|
||||
- [gitlink-branch](../SKILL.md) — 分支操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
# branch +unprotect
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
移除分支保护规则,允许分支被直接修改。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 移除分支保护
|
||||
gitlink-cli branch +unprotect --name main
|
||||
|
||||
# 指定仓库移除分支保护
|
||||
gitlink-cli branch +unprotect --name main --owner someone --repo myrepo
|
||||
|
||||
# 移除开发分支保护
|
||||
gitlink-cli branch +unprotect --name develop
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | **是** | 要移除保护的分支名称 |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
DELETE /{owner}/{repo}/protected_branches/{branch_name}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"branch_name": "main",
|
||||
"protected": false,
|
||||
"message": "Branch protection removed successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the branch name to unprotect with the user.
|
||||
2. **Execute** `gitlink-cli branch +unprotect --name <name>`.
|
||||
3. **Report** the unprotection result.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent before executing. This will allow direct pushes to the branch.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **紧急修复**:临时允许直接推送紧急修复
|
||||
- **分支重组**:调整分支保护策略
|
||||
- **迁移工作流**:从 PR 工作流切换到直接推送
|
||||
- **权限调整**:根据团队需求调整保护规则
|
||||
|
||||
## What Unprotection Means
|
||||
|
||||
移除分支保护后,以下操作将被允许:
|
||||
- ✅ **允许操作**:直接推送代码
|
||||
- ✅ **允许操作**:强制推送
|
||||
- ✅ **允许操作**:删除分支
|
||||
- ✅ **允许操作**:修改历史
|
||||
|
||||
## Risks and Considerations
|
||||
|
||||
⚠️ **风险提醒**:
|
||||
- 失去 PR 代码审查机制
|
||||
- 可能直接推送到关键分支
|
||||
- 增加代码冲突和错误风险
|
||||
- 影响代码质量和稳定性
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **谨慎使用**:仅在确有需要时移除保护
|
||||
2. **临时移除**:考虑临时移除后重新保护
|
||||
3. **团队沟通**:移除保护前通知团队成员
|
||||
4. **重新保护**:完成操作后及时恢复保护
|
||||
|
||||
## When to Use
|
||||
|
||||
**适合移除保护的情况:**
|
||||
- 紧急修复需要快速部署
|
||||
- 仓库结构重组或迁移
|
||||
- 测试和验证工作流
|
||||
- 小团队内部协作
|
||||
|
||||
**不适合移除保护的情况:**
|
||||
- 有 PR 审查需求
|
||||
- 多人协作的大型项目
|
||||
- 需要严格代码质量控制
|
||||
- 生产环境的关键分支
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 分支不存在 | 使用 `branch +list` 确认分支存在 |
|
||||
| `404` | 未被保护 | 分支当前没有保护规则 |
|
||||
| `403` | 权限不足 | 确认有管理权限 |
|
||||
| `400` | 路径问题 | 含 `/` 的分支名可能需要通过 Web 操作 |
|
||||
|
||||
## Limitations
|
||||
|
||||
- ⚠️ **路径限制**:含 `/` 的分支名(如 `feature/my-branch`)可能无法通过 CLI 移除保护
|
||||
- ⚠️ **API 限制**:某些特殊分支可能需要通过 Web 页面操作
|
||||
- ⚠️ **权限要求**:需要管理员或协作者权限
|
||||
|
||||
## Safety Workflow
|
||||
|
||||
推荐的移除保护工作流:
|
||||
|
||||
```bash
|
||||
# 1. 查看当前保护状态
|
||||
gitlink-cli branch +list --format json | jq '.data.branches[] | select(.protected)'
|
||||
|
||||
# 2. 确认要移除保护的分支
|
||||
gitlink-cli branch +list --format json | jq '.data.branches[] | select(.name=="main") | .protected'
|
||||
|
||||
# 3. 移除分支保护
|
||||
gitlink-cli branch +unprotect --name main
|
||||
|
||||
# 4. 验证移除结果
|
||||
gitlink-cli branch +list --format json | jq '.data.branches[] | select(.name=="main") | .protected'
|
||||
|
||||
# 5. 完成操作后重新保护
|
||||
gitlink-cli branch +protect --name main
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- 移除保护前,建议先检查当前的保护状态
|
||||
- 考虑设置定时提醒,确保及时恢复保护
|
||||
- 对于重要分支,建议使用 Web UI 确认移除保护
|
||||
- 记录移除保护的原因和时间,便于审计
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
团队协作时的建议:
|
||||
|
||||
1. **提前沟通**:在移除保护前通知所有团队成员
|
||||
2. **说明原因**:向团队解释为什么需要移除保护
|
||||
3. **时间限制**:设定移除保护的时间限制
|
||||
4. **操作文档**:记录移除保护的操作和原因
|
||||
5. **及时恢复**:完成操作后立即恢复保护
|
||||
|
||||
## References
|
||||
|
||||
- [branch +protect](branch-protect.md) — 设置分支保护
|
||||
- [branch +list](branch-list.md) — 列出分支并查看保护状态
|
||||
- [gitlink-branch](../SKILL.md) — 分支操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# ci +builds
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
列出仓库的所有 CI/CD 构建记录,支持分页查询。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 查看当前仓库的构建列表
|
||||
gitlink-cli ci +builds
|
||||
|
||||
# 指定仓库查看构建
|
||||
gitlink-cli ci +builds --owner myuser --repo myrepo
|
||||
|
||||
# 分页查询
|
||||
gitlink-cli ci +builds --page 2 --limit 10
|
||||
|
||||
# 输出为 JSON 格式
|
||||
gitlink-cli ci +builds --format json
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--page, -p` | 否 | 页码(默认 `1`) |
|
||||
| `--limit, -l` | 否 | 每页条数(默认 `20`) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET /{owner}/{repo}/builds?page=1&limit=20
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"builds": [
|
||||
{
|
||||
"id": 42,
|
||||
"build_number": 42,
|
||||
"status": "success",
|
||||
"started_at": "2026-01-01T10:00:00Z",
|
||||
"duration": 125,
|
||||
"commit": {
|
||||
"sha": "abc123...",
|
||||
"message": "Fix bug in authentication",
|
||||
"author": "developer@example.com"
|
||||
},
|
||||
"branch": "feature/auth-fix",
|
||||
"stages": [
|
||||
{
|
||||
"stage_number": 1,
|
||||
"stage_name": "build",
|
||||
"status": "success"
|
||||
},
|
||||
{
|
||||
"stage_number": 2,
|
||||
"stage_name": "test",
|
||||
"status": "success"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"build_number": 41,
|
||||
"status": "failed",
|
||||
"started_at": "2026-01-01T09:30:00Z",
|
||||
"duration": 45,
|
||||
"commit": {
|
||||
"sha": "def456...",
|
||||
"message": "Add new feature",
|
||||
"author": "developer@example.com"
|
||||
},
|
||||
"branch": "develop",
|
||||
"stages": [
|
||||
{
|
||||
"stage_number": 1,
|
||||
"stage_name": "build",
|
||||
"status": "failed"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_count": 156
|
||||
},
|
||||
"meta": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total_count": 156
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Resolve** owner and repo (from git remote or flags).
|
||||
2. **Execute** `gitlink-cli ci +builds`.
|
||||
3. **Display** builds in the requested format.
|
||||
|
||||
> [!NOTE]
|
||||
> This is a **Read Operation** — no confirmation needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **构建历史查看**:查看仓库的构建历史和状态
|
||||
- **问题排查**:查找失败的构建进行分析
|
||||
- **构建监控**:监控 CI/CD 系统的运行状态
|
||||
- **自动化脚本**:结合 JSON 格式输出进行构建分析
|
||||
|
||||
## Build Status
|
||||
|
||||
构建状态类型:
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `pending` | 等待执行 |
|
||||
| `running` | 正在执行 |
|
||||
| `success` | 构建成功 |
|
||||
| `failed` | 构建失败 |
|
||||
| `cancelled` | 构建取消 |
|
||||
| `skipped` | 构建跳过 |
|
||||
|
||||
## Data Analysis
|
||||
|
||||
使用 JSON 输出进行构建分析:
|
||||
|
||||
```bash
|
||||
# 查看最近10次构建的成功率
|
||||
gitlink-cli ci +builds --format json --limit 10 | \
|
||||
jq '[.data.builds[] | select(.status=="success")] | length / 10 * 100'
|
||||
|
||||
# 查看失败的构建
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '.data.builds[] | select(.status=="failed")'
|
||||
|
||||
# 查看平均构建时间
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '[.data.builds[].duration] | add / length'
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- 使用 `--format json` 可以更好地解析和分析构建数据
|
||||
- 构建列表包含详细的提交信息和分支信息
|
||||
- 支持分页,适合构建历史较多的仓库
|
||||
- 结合 `ci +logs` 可以深入分析构建失败原因
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
结合其他 CI 命令的典型工作流:
|
||||
|
||||
```bash
|
||||
# 1. 查看构建列表
|
||||
gitlink-cli ci +builds
|
||||
|
||||
# 2. 查看失败构建的日志
|
||||
gitlink-cli ci +logs --build 42
|
||||
|
||||
# 3. 重启失败的构建
|
||||
gitlink-cli ci +restart --build 42
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [ci +logs](ci-logs.md) — 查看构建日志
|
||||
- [ci +restart](ci-restart.md) — 重启构建
|
||||
- [ci +stop](ci-stop.md) — 停止构建
|
||||
- [gitlink-ci](../SKILL.md) — CI/CD 操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
# ci +logs
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
查看指定 CI 构建的详细日志输出。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 查看构建日志
|
||||
gitlink-cli ci +logs --build 42
|
||||
|
||||
# 查看特定阶段的日志
|
||||
gitlink-cli ci +logs --build 42 --stage 2
|
||||
|
||||
# 查看特定步骤的日志
|
||||
gitlink-cli ci +logs --build 42 --stage 2 --step 3
|
||||
|
||||
# 指定仓库查看日志
|
||||
gitlink-cli ci +logs --build 42 --owner myuser --repo myrepo
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--build, -b` | **是** | 构建编号 |
|
||||
| `--stage, -s` | 否 | 阶段编号(默认 `1`) |
|
||||
| `--step` | 否 | 步骤编号(默认 `1`) |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET /{owner}/{repo}/builds/{build}/logs/{stage}/{step}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"build_number": 42,
|
||||
"stage_number": 2,
|
||||
"step_number": 1,
|
||||
"log_content": "Running tests...\nTest suite started...\n[OK] Test authentication\n[OK] Test database connection\n[FAILED] Test API endpoint\n\nTests completed: 2/3 passed",
|
||||
"stage_name": "test",
|
||||
"step_name": "run_tests",
|
||||
"timestamp": "2026-01-01T10:05:30Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the build number with the user (can use `ci +builds` to list).
|
||||
2. **Execute** `gitlink-cli ci +logs --build <number> [--stage <n>] [--step <n>]`.
|
||||
3. **Display** the log content.
|
||||
|
||||
> [!NOTE]
|
||||
> This is a **Read Operation** — no confirmation needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **问题排查**:查看构建失败的具体原因
|
||||
- **性能分析**:分析构建过程中的性能瓶颈
|
||||
- **调试输出**:查看代码运行时的调试信息
|
||||
- **监控执行**:实时跟踪构建执行状态
|
||||
|
||||
## CI Pipeline Structure
|
||||
|
||||
典型的 CI/CD 流水线结构:
|
||||
|
||||
```
|
||||
Stage 1: Build
|
||||
├── Step 1: Install dependencies
|
||||
├── Step 2: Build application
|
||||
└── Step 3: Run linters
|
||||
|
||||
Stage 2: Test
|
||||
├── Step 1: Run unit tests
|
||||
├── Step 2: Run integration tests
|
||||
└── Step 3: Generate coverage report
|
||||
|
||||
Stage 3: Deploy
|
||||
├── Step 1: Build deployment package
|
||||
└── Step 2: Deploy to server
|
||||
```
|
||||
|
||||
## Log Analysis
|
||||
|
||||
日志分析技巧:
|
||||
|
||||
```bash
|
||||
# 查看构建日志
|
||||
gitlink-cli ci +logs --build 42 --stage 2 --step 1
|
||||
|
||||
# 结合 grep 过滤关键错误
|
||||
gitlink-cli ci +logs --build 42 --format json | \
|
||||
jq '.data.log_content' | grep "ERROR"
|
||||
|
||||
# 查看完整日志流
|
||||
gitlink-cli ci +logs --build 42 --format json | \
|
||||
jq -r '.data.log_content'
|
||||
```
|
||||
|
||||
## Stage and Step Navigation
|
||||
|
||||
查看不同阶段的日志:
|
||||
|
||||
```bash
|
||||
# Stage 1: Build stage
|
||||
gitlink-cli ci +logs --build 42 --stage 1 --step 1
|
||||
|
||||
# Stage 2: Test stage
|
||||
gitlink-cli ci +logs --build 42 --stage 2 --step 1
|
||||
|
||||
# Stage 3: Deploy stage
|
||||
gitlink-cli ci +logs --build 42 --stage 3 --step 1
|
||||
```
|
||||
|
||||
## Common Log Patterns
|
||||
|
||||
常见日志模式:
|
||||
|
||||
| 模式 | 含义 |
|
||||
|------|------|
|
||||
| `[ERROR]` | 错误信息 |
|
||||
| `[FAILED]` | 测试或步骤失败 |
|
||||
| `[WARN]` | 警告信息 |
|
||||
| `[OK]` | 操作成功 |
|
||||
| `Running...` | 正在执行 |
|
||||
| `Completed` | 执行完成 |
|
||||
|
||||
## Tips
|
||||
|
||||
- 先使用 `ci +builds` 确认构建编号
|
||||
- 构建通常包含多个阶段,需要指定正确的阶段编号
|
||||
- 日志内容可能很长,建议使用 `--format json` 便于解析
|
||||
- 结合构建状态可以快速定位问题
|
||||
|
||||
## Troubleshooting Workflow
|
||||
|
||||
典型的故障排查工作流:
|
||||
|
||||
```bash
|
||||
# 1. 查看构建列表,找到失败的构建
|
||||
gitlink-cli ci +builds | grep "failed"
|
||||
|
||||
# 2. 查看失败构建的详细状态
|
||||
gitlink-cli ci +builds --build 42 --format json | \
|
||||
jq '.data.builds[] | .stages[]'
|
||||
|
||||
# 3. 查看失败阶段的日志
|
||||
gitlink-cli ci +logs --build 42 --stage 2 --step 1
|
||||
|
||||
# 4. 根据日志信息修复问题
|
||||
|
||||
# 5. 重启构建
|
||||
gitlink-cli ci +restart --build 42
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 构建不存在 | 检查构建编号是否正确 |
|
||||
| `404` | 阶段或步骤不存在 | 确认阶段和步骤编号 |
|
||||
| `403` | 权限不足 | 确认有查看该仓库构建的权限 |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
高级用法示例:
|
||||
|
||||
```bash
|
||||
# 导出构建日志到文件
|
||||
gitlink-cli ci +logs --build 42 --format json | \
|
||||
jq -r '.data.log_content' > build_42_logs.txt
|
||||
|
||||
# 分析日志中的错误模式
|
||||
gitlink-cli ci +logs --build 42 --format json | \
|
||||
jq -r '.data.log_content' | grep -c "ERROR"
|
||||
|
||||
# 查看所有阶段的日志(循环)
|
||||
for stage in {1..3}; do
|
||||
echo "=== Stage $stage ==="
|
||||
gitlink-cli ci +logs --build 42 --stage $stage --step 1
|
||||
done
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [ci +builds](ci-list.md) — 查看构建列表
|
||||
- [ci +restart](ci-restart.md) — 重启构建
|
||||
- [gitlink-ci](../SKILL.md) — CI/CD 操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
# ci +restart
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
重新启动失败的或取消的 CI 构建。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 重启构建
|
||||
gitlink-cli ci +restart --build 42
|
||||
|
||||
# 指定仓库重启构建
|
||||
gitlink-cli ci +restart --build 42 --owner myuser --repo myrepo
|
||||
|
||||
# 重启失败的构建(JSON 输出)
|
||||
gitlink-cli ci +restart --build 42 --format json
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--build, -b` | **是** | 构建编号 |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
POST /{owner}/{repo}/builds/{build}/restart
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"old_build_number": 42,
|
||||
"new_build_number": 43,
|
||||
"status": "pending",
|
||||
"message": "Build restarted successfully",
|
||||
"triggered_at": "2026-01-01T11:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the build number to restart with the user.
|
||||
2. **Check** the current build status (optional, using `ci +builds`).
|
||||
3. **Execute** `gitlink-cli ci +restart --build <number>`.
|
||||
4. **Report** the restart result and new build number.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent before executing.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **失败重试**:构建因临时问题失败后重试
|
||||
- **取消后重新执行**:构建被取消后需要重新执行
|
||||
- **代码修复后验证**:修复代码后重新验证构建
|
||||
- **环境问题恢复**:CI 环境问题恢复后重新构建
|
||||
|
||||
## When to Restart
|
||||
|
||||
**适合重启的情况:**
|
||||
- ✅ 构建因临时网络问题失败
|
||||
- ✅ 依赖服务暂时不可用
|
||||
- ✅ 代码修复后需要重新验证
|
||||
- ✅ CI 环境问题已解决
|
||||
|
||||
**不适合重启的情况:**
|
||||
- ❌ 代码存在严重错误
|
||||
- ❌ 测试用例本身有问题
|
||||
- ❌ 构建配置需要修改
|
||||
- ❌ 依赖库版本不兼容
|
||||
|
||||
## Restart Behavior
|
||||
|
||||
重启构建的行为特点:
|
||||
|
||||
| 方面 | 说明 |
|
||||
|------|------|
|
||||
| **新构建编号** | 重启会创建新的构建编号 |
|
||||
| **相同代码** | 使用相同的提交代码 |
|
||||
| **相同环境** | 使用相同的构建环境 |
|
||||
| **独立日志** | 新构建有独立的日志记录 |
|
||||
| **状态继承** | 不会继承原构建的状态 |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **查看日志**:重启前先查看失败原因
|
||||
2. **修复问题**:如果是代码问题,先修复再重启
|
||||
3. **监控新构建**:重启后监控新构建的执行状态
|
||||
4. **资源考虑**:频繁重启会消耗 CI 资源
|
||||
|
||||
## Troubleshooting Workflow
|
||||
|
||||
典型的故障排查和重启流程:
|
||||
|
||||
```bash
|
||||
# 1. 查看构建列表,找到失败的构建
|
||||
gitlink-cli ci +builds | grep "failed"
|
||||
|
||||
# 2. 查看失败构建的详细状态
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '.data.builds[] | select(.build_number==42)'
|
||||
|
||||
# 3. 查看失败阶段的日志
|
||||
gitlink-cli ci +logs --build 42 --stage 2 --step 1
|
||||
|
||||
# 4. 分析日志,确定失败原因
|
||||
|
||||
# 5. 如果是临时问题,重启构建
|
||||
gitlink-cli ci +restart --build 42
|
||||
|
||||
# 6. 如果是代码问题,修复后重启
|
||||
# (先修复代码,然后)
|
||||
gitlink-cli ci +restart --build 42
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 构建不存在 | 检查构建编号是否正确 |
|
||||
| `400` | 构建正在运行 | 正在运行的构建无法重启 |
|
||||
| `403` | 权限不足 | 确认有操作该仓库构建的权限 |
|
||||
| `429` | 重启次数过多 | 短时间内重启次数过多,等待后重试 |
|
||||
|
||||
## Pre-Restart Checklist
|
||||
|
||||
重启前检查清单:
|
||||
|
||||
- [ ] 确认构建编号正确
|
||||
- [ ] 查看失败日志,了解失败原因
|
||||
- [ ] 确认问题已解决(如果是代码问题)
|
||||
- [ ] 检查 CI 系统状态
|
||||
- [ ] 确认有足够的 CI 资源
|
||||
- [ ] 考虑是否需要修改构建配置
|
||||
|
||||
## Post-Restart Actions
|
||||
|
||||
重启后的后续操作:
|
||||
|
||||
```bash
|
||||
# 1. 重启构建
|
||||
gitlink-cli ci +restart --build 42
|
||||
|
||||
# 2. 获取新构建编号
|
||||
gitlink-cli ci +restart --build 42 --format json | \
|
||||
jq '.data.new_build_number'
|
||||
|
||||
# 3. 监控新构建状态
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '.data.builds[0]'
|
||||
|
||||
# 4. 查看新构建的日志(如需要)
|
||||
gitlink-cli ci +logs --build 43 --stage 1 --step 1
|
||||
```
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
团队协作时的建议:
|
||||
|
||||
1. **沟通确认**:重启构建前通知相关团队成员
|
||||
2. **记录原因**:记录重启的原因和时间
|
||||
3. **状态更新**:及时更新构建状态给团队
|
||||
4. **结果分享**:重启完成后分享结果
|
||||
|
||||
## Tips
|
||||
|
||||
- 重启会创建新的构建编号,原构建历史仍保留
|
||||
- 重启前建议先查看日志,确认问题性质
|
||||
- 对于重复失败的情况,建议先修复根本原因
|
||||
- 可以通过 `ci +builds` 查看重启后的新构建状态
|
||||
|
||||
## Cost Considerations
|
||||
|
||||
使用注意事项:
|
||||
|
||||
- ⚠️ **资源消耗**:每次重启都会消耗 CI 资源
|
||||
- ⚠️ **时间成本**:重新执行完整的构建流程
|
||||
- ⚠️ **排队时间**:新构建可能需要排队等待
|
||||
- ⚠️ **频繁重启**:避免无意义的频繁重启
|
||||
|
||||
## References
|
||||
|
||||
- [ci +builds](ci-list.md) — 查看构建列表
|
||||
- [ci +logs](ci-logs.md) — 查看构建日志
|
||||
- [ci +stop](ci-stop.md) — 停止构建
|
||||
- [gitlink-ci](../SKILL.md) — CI/CD 操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
# ci +stop
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
停止正在运行的 CI 构建。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 停止构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
|
||||
# 指定仓库停止构建
|
||||
gitlink-cli ci +stop --build 42 --owner myuser --repo myrepo
|
||||
|
||||
# 停止构建(JSON 输出)
|
||||
gitlink-cli ci +stop --build 42 --format json
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--build, -b` | **是** | 构建编号 |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
DELETE /{owner}/{repo}/builds/{build}/stop
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"build_number": 42,
|
||||
"status": "cancelled",
|
||||
"message": "Build stopped successfully",
|
||||
"stopped_at": "2026-01-01T11:30:00Z",
|
||||
"duration": 180
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the build number to stop with the user.
|
||||
2. **Check** the current build status (ensure it's running).
|
||||
3. **Execute** `gitlink-cli ci +stop --build <number>`.
|
||||
4. **Report** the stop result.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent before executing. This will terminate a running build.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **错误停止**:构建出现错误需要立即停止
|
||||
- **资源释放**:释放 CI 资源给其他构建
|
||||
- **配置错误**:构建配置错误需要停止
|
||||
- **测试中止**:测试过程中发现问题需要中止
|
||||
- **时间限制**:构建时间过长需要停止
|
||||
|
||||
## When to Stop
|
||||
|
||||
**适合停止的情况:**
|
||||
- ✅ 构建明显出现错误,继续执行无意义
|
||||
- ✅ 发现严重bug,需要立即停止
|
||||
- ✅ 构建配置错误,需要修改后重新执行
|
||||
- ✅ 误触发构建,需要立即取消
|
||||
- ✅ 构建时间过长,超出预期
|
||||
|
||||
**不适合停止的情况:**
|
||||
- ❌ 构建接近完成
|
||||
- ❌ 仅为节省时间而停止正常构建
|
||||
- ❌ 不确定构建是否有问题
|
||||
|
||||
## Stop Behavior
|
||||
|
||||
停止构建的行为特点:
|
||||
|
||||
| 方面 | 说明 |
|
||||
|------|------|
|
||||
| **立即停止** | 通常会立即中断构建执行 |
|
||||
| **状态变更** | 构建状态变为 `cancelled` |
|
||||
| **资源释放** | 释放 CI 计算资源 |
|
||||
| **日志保留** | 已执行的日志会保留 |
|
||||
| **不可恢复** | 停止的构建无法恢复执行 |
|
||||
|
||||
## Safety Considerations
|
||||
|
||||
停止构建前考虑:
|
||||
|
||||
- ⚠️ **进度损失**:已执行的进度会丢失
|
||||
- ⚠️ **资源浪费**:已消耗的资源无法回收
|
||||
- ⚠️ **团队影响**:可能影响其他依赖此构建的任务
|
||||
- ⚠️ **重新执行**:需要重新启动完整的构建
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **确认状态**:停止前确认构建确实在运行
|
||||
2. **评估影响**:考虑停止对其他流程的影响
|
||||
3. **记录原因**:记录停止构建的原因
|
||||
4. **后续处理**:计划停止后的后续操作
|
||||
|
||||
## Stop Workflow
|
||||
|
||||
典型的停止构建工作流:
|
||||
|
||||
```bash
|
||||
# 1. 查看运行中的构建
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '.data.builds[] | select(.status=="running")'
|
||||
|
||||
# 2. 确认要停止的构建编号
|
||||
gitlink-cli ci +builds | grep "running"
|
||||
|
||||
# 3. 停止构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
|
||||
# 4. 验证停止状态
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '.data.builds[] | select(.build_number==42) | .status'
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `404` | 构建不存在 | 检查构建编号是否正确 |
|
||||
| `400` | 构建已完成 | 构建已经完成或停止,无法再停止 |
|
||||
| `403` | 权限不足 | 确认有操作该仓库构建的权限 |
|
||||
| `409` | 构建已完成 | 构建已经自然结束 |
|
||||
|
||||
## Pre-Stop Checklist
|
||||
|
||||
停止前检查清单:
|
||||
|
||||
- [ ] 确认构建编号正确
|
||||
- [ ] 确认构建正在运行
|
||||
- [ ] 评估停止的影响范围
|
||||
- [ ] 确认停止原因合理
|
||||
- [ ] 考虑后续处理方案
|
||||
- [ ] 通知相关团队成员
|
||||
|
||||
## Post-Stop Actions
|
||||
|
||||
停止后的后续操作:
|
||||
|
||||
```bash
|
||||
# 1. 停止构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
|
||||
# 2. 查看停止状态
|
||||
gitlink-cli ci +builds --format json | \
|
||||
jq '.data.builds[] | select(.build_number==42)'
|
||||
|
||||
# 3. 查看已执行的日志
|
||||
gitlink-cli ci +logs --build 42 --stage 1 --step 1
|
||||
|
||||
# 4. 根据需要重启构建
|
||||
gitlink-cli ci +restart --build 42
|
||||
```
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
常见使用场景:
|
||||
|
||||
### 场景1:发现严重错误
|
||||
```bash
|
||||
# 查看运行中的构建
|
||||
gitlink-cli ci +builds | grep "running"
|
||||
|
||||
# 查看日志发现严重错误
|
||||
gitlink-cli ci +logs --build 42 --stage 2 --step 1
|
||||
|
||||
# 立即停止构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
```
|
||||
|
||||
### 场景2:误触发构建
|
||||
```bash
|
||||
# 发现误触发了构建
|
||||
gitlink-cli ci +builds | grep "running"
|
||||
|
||||
# 立即停止误触发的构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
```
|
||||
|
||||
### 场景3:配置错误
|
||||
```bash
|
||||
# 发现构建配置错误
|
||||
gitlink-cli ci +logs --build 42 --stage 1 --step 1
|
||||
|
||||
# 停止当前构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
|
||||
# 修复配置后重新构建
|
||||
# (修复配置)
|
||||
gitlink-cli ci +restart --build 42
|
||||
```
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
团队协作时的建议:
|
||||
|
||||
1. **及时通知**:停止构建前通知相关团队成员
|
||||
2. **说明原因**:向团队解释为什么需要停止构建
|
||||
3. **状态同步**:更新项目管理系统中的构建状态
|
||||
4. **后续计划**:告知团队停止后的处理计划
|
||||
|
||||
## Tips
|
||||
|
||||
- 停止前建议先确认构建状态,避免重复操作
|
||||
- 查看构建日志可以帮助判断是否值得停止
|
||||
- 停止后可以考虑是否需要重启或修复后重新构建
|
||||
- 对于长时间运行的构建,定期检查状态可能更合适
|
||||
|
||||
## Alternatives
|
||||
|
||||
替代方案考虑:
|
||||
|
||||
| 情况 | 停止 | 等待完成 | 其他方案 |
|
||||
|------|------|----------|----------|
|
||||
| 严重错误 | ✅ 推荐 | ❌ 不推荐 | 修复后重启 |
|
||||
| 临时问题 | ⚠️ 可选 | ✅ 推荐 | 等待自动恢复 |
|
||||
| 配置错误 | ✅ 推荐 | ❌ 不推荐 | 修复配置后重启 |
|
||||
| 时间过长 | ⚠️ 可选 | ✅ 推荐 | 优化构建流程 |
|
||||
|
||||
## References
|
||||
|
||||
- [ci +builds](ci-list.md) — 查看构建列表
|
||||
- [ci +logs](ci-logs.md) — 查看构建日志
|
||||
- [ci +restart](ci-restart.md) — 重启构建
|
||||
- [gitlink-ci](../SKILL.md) — CI/CD 操作总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
# PM 看板管理
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **注意**:PM 功能需要项目开启 PM 模块,并通过项目 ID 访问。
|
||||
|
||||
GitLink PM 看板功能提供项目任务的可视化管理,支持任务的拖拽、状态管理和团队协作。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 查看项目看板
|
||||
gitlink-cli api GET /pm/dashboards --query 'project_id=123'
|
||||
|
||||
# 查看当前仓库的项目 ID
|
||||
gitlink-cli repo +info --format json | jq '.data.project_id'
|
||||
|
||||
# 组合命令:自动获取项目 ID 并查看看板
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
gitlink-cli api GET /pm/dashboards --query "project_id=$PROJECT_ID"
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `project_id` | **是** | 项目 ID(通过 `repo +info` 获取) |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET /api/pm/dashboards?project_id={project_id}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"dashboards": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "开发看板",
|
||||
"description": "主开发任务看板",
|
||||
"project_id": 123,
|
||||
"columns": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "待处理",
|
||||
"position": 1,
|
||||
"issue_count": 5
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "进行中",
|
||||
"position": 2,
|
||||
"issue_count": 3
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "已完成",
|
||||
"position": 3,
|
||||
"issue_count": 8
|
||||
}
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"id": 456,
|
||||
"subject": "实现用户认证功能",
|
||||
"status_id": 1,
|
||||
"priority_id": 2,
|
||||
"assigned_to": "developer1",
|
||||
"column_id": 2,
|
||||
"position": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"total_count": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get Project ID** using `repo +info`.
|
||||
2. **Execute** `gitlink-cli api GET /pm/dashboards --query 'project_id=<id>'`.
|
||||
3. **Display** kanban board information.
|
||||
|
||||
> [!NOTE]
|
||||
> This is a **Read Operation** — no confirmation needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **任务可视化**:直观查看项目任务分布
|
||||
- **进度跟踪**:实时监控任务进展情况
|
||||
- **资源分配**:查看团队成员的工作负载
|
||||
- **流程管理**:管理任务的流转和状态变更
|
||||
|
||||
## Kanban Board Structure
|
||||
|
||||
典型看板结构:
|
||||
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────┐
|
||||
│ 待处理 │ 进行中 │ 已完成 │
|
||||
│ [5 tasks] │ [3 tasks] │ [8 tasks] │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ Task 1 │ Task 6 │ Task 11 │
|
||||
│ Task 2 │ Task 7 │ Task 12 │
|
||||
│ Task 3 │ Task 8 │ Task 13 │
|
||||
│ Task 4 │ Task 9 │ Task 14 │
|
||||
│ Task 5 │ Task 10 │ Task 15 │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
看板常用操作:
|
||||
|
||||
### 查看任务分布
|
||||
```bash
|
||||
# 查看各列的任务数量
|
||||
gitlink-cli api GET /pm/dashboards --query 'project_id=123' --format json | \
|
||||
jq '.data.dashboards[0].columns[] | {name: .name, count: .issue_count}'
|
||||
```
|
||||
|
||||
### 查看特定任务
|
||||
```bash
|
||||
# 查看"进行中"的任务
|
||||
gitlink-cli api GET /pm/dashboards --query 'project_id=123' --format json | \
|
||||
jq '.data.dashboards[0].issues[] | select(.column_id==2)'
|
||||
```
|
||||
|
||||
### 统计工作负载
|
||||
```bash
|
||||
# 按人员统计任务数量
|
||||
gitlink-cli api GET /pm/dashboards --query 'project_id=123' --format json | \
|
||||
jq '.data.dashboards[0].issues[] | group_by(.assigned_to) | map({assigned_to: .[0].assigned_to, count: length})'
|
||||
```
|
||||
|
||||
## Task Management
|
||||
|
||||
任务管理最佳实践:
|
||||
|
||||
1. **列管理**:合理设置任务列(如:待处理、进行中、已完成)
|
||||
2. **限制数量**:对"进行中"列设置 WIP 限制
|
||||
3. **定期清理**:及时移动已完成任务到相应列
|
||||
4. **优先级标记**:使用标签和优先级标识重要任务
|
||||
|
||||
## Analysis Examples
|
||||
|
||||
看板数据分析示例:
|
||||
|
||||
```bash
|
||||
# 1. 获取项目 ID
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
|
||||
# 2. 查看完整看板数据
|
||||
gitlink-cli api GET /pm/dashboards --query "project_id=$PROJECT_ID" --format json
|
||||
|
||||
# 3. 分析任务瓶颈(找出任务最多的列)
|
||||
gitlink-cli api GET /pm/dashboards --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '.data.dashboards[0].columns | sort_by(.issue_count) | reverse | .[0]'
|
||||
|
||||
# 4. 计算完成率
|
||||
gitlink-cli api GET /pm/dashboards --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '[.data.dashboards[0].columns[] | select(.name=="已完成")] | .[0].issue_count /
|
||||
[.data.dashboards[0].columns[].issue_count] | add * 100'
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- 看板数据可以帮助识别项目瓶颈
|
||||
- 定期查看看板可以保持项目进度的可视化
|
||||
- 结合 Issue 操作可以实现完整的任务管理流程
|
||||
- 使用 JSON 格式输出便于自动化分析
|
||||
|
||||
## Integration with Other Features
|
||||
|
||||
与其他功能集成:
|
||||
|
||||
```bash
|
||||
# 看板 + Issue 操作
|
||||
# 1. 查看看板中的任务
|
||||
gitlink-cli api GET /pm/dashboards --query 'project_id=123' --format json
|
||||
|
||||
# 2. 查看特定任务详情
|
||||
gitlink-cli issue +view --issue 456
|
||||
|
||||
# 3. 更新任务状态
|
||||
gitlink-cli issue +update --issue 456 --status_id 3
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `400` | PM 模块未开启 | 联系项目管理员开启 PM 模块 |
|
||||
| `404` | 项目不存在 | 检查 project_id 是否正确 |
|
||||
| `403` | 权限不足 | 确认有查看该项目的权限 |
|
||||
| `404` | 看板不存在 | 该项目可能没有配置看板 |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
使用 PM 功能的前置条件:
|
||||
|
||||
1. **PM 模块开启**:项目需要开启 PM 功能模块
|
||||
2. **有效项目 ID**:需要正确的项目 ID
|
||||
3. **访问权限**:需要该项目的访问权限
|
||||
4. **看板配置**:项目需要有配置的看板
|
||||
|
||||
## Setup Workflow
|
||||
|
||||
PM 功能设置流程:
|
||||
|
||||
```bash
|
||||
# 1. 检查项目是否开启 PM
|
||||
gitlink-cli repo +info --format json | jq '.data.has_pm'
|
||||
|
||||
# 2. 获取项目 ID
|
||||
gitlink-cli repo +info --format json | jq '.data.project_id'
|
||||
|
||||
# 3. 查看看板配置
|
||||
gitlink-cli api GET /pm/dashboards --query 'project_id=123'
|
||||
|
||||
# 4. 如需配置看板,通过 GitLink 网页端操作
|
||||
# https://www.gitlink.org.cn/{owner}/{repo}/project_modules
|
||||
```
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
团队协作建议:
|
||||
|
||||
1. **定期更新**:团队成员定期更新任务状态
|
||||
2. **明确规范**:制定看板使用规范和列定义
|
||||
3. **WIP 限制**:设置进行中任务的数量限制
|
||||
4. **定期回顾**:定期回顾看板数据,优化流程
|
||||
|
||||
## References
|
||||
|
||||
- [pm-sprint](pm-sprint.md) — Sprint 管理
|
||||
- [pm-report](pm-report.md) — 周报生成
|
||||
- [gitlink-pm](../SKILL.md) — 项目管理总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [repo +info](../../gitlink-repo/references/repo-info.md) — 获取仓库信息
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
# PM 周报生成
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **注意**:PM 功能需要项目开启 PM 模块,并通过项目 ID 访问。
|
||||
|
||||
GitLink PM 周报功能提供项目一周工作情况的自动汇总,包括 Issue、Pull Request、提交记录等数据。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 查看周报数据
|
||||
gitlink-cli api GET /pm/weekly_issues --query 'project_id=123'
|
||||
|
||||
# 查看当前仓库的项目 ID
|
||||
gitlink-cli repo +info --format json | jq '.data.project_id'
|
||||
|
||||
# 组合命令:自动获取项目 ID 并查看周报
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID"
|
||||
|
||||
# 查看特定日期范围的周报
|
||||
gitlink-cli api GET /pm/weekly_issues --query 'project_id=123&start_date=2026-01-01&end_date=2026-01-07'
|
||||
|
||||
# 查看 Issue 标签统计
|
||||
gitlink-cli api GET /pm/issue_tags --query 'project_id=123'
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `project_id` | **是** | 项目 ID(通过 `repo +info` 获取) |
|
||||
| `start_date` | 否 | 开始日期(格式:YYYY-MM-DD) |
|
||||
| `end_date` | 否 | 结束日期(格式:YYYY-MM-DD) |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET /api/pm/weekly_issues?project_id={project_id}&start_date={start_date}&end_date={end_date}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"period": {
|
||||
"start_date": "2026-01-01",
|
||||
"end_date": "2026-01-07",
|
||||
"week_number": 1
|
||||
},
|
||||
"summary": {
|
||||
"total_issues": 15,
|
||||
"new_issues": 8,
|
||||
"closed_issues": 5,
|
||||
"in_progress_issues": 2,
|
||||
"total_prs": 6,
|
||||
"merged_prs": 4,
|
||||
"total_commits": 42
|
||||
},
|
||||
"issues": [
|
||||
{
|
||||
"id": 456,
|
||||
"subject": "实现用户认证功能",
|
||||
"status": "closed",
|
||||
"author": "developer1",
|
||||
"assigned_to": "developer2",
|
||||
"created_at": "2026-01-02T10:00:00Z",
|
||||
"closed_at": "2026-01-05T15:30:00Z",
|
||||
"labels": ["feature", "authentication"]
|
||||
}
|
||||
],
|
||||
"pull_requests": [
|
||||
{
|
||||
"id": 123,
|
||||
"title": "Feature: User authentication",
|
||||
"status": "merged",
|
||||
"author": "developer1",
|
||||
"merged_at": "2026-01-05T16:00:00Z",
|
||||
"additions": 245,
|
||||
"deletions": 18
|
||||
}
|
||||
],
|
||||
"commits": [
|
||||
{
|
||||
"id": "abc123",
|
||||
"message": "Implement user login",
|
||||
"author": "developer1",
|
||||
"committed_date": "2026-01-03T14:20:00Z"
|
||||
}
|
||||
],
|
||||
"team_contributions": [
|
||||
{
|
||||
"developer": "developer1",
|
||||
"issues_created": 3,
|
||||
"issues_closed": 2,
|
||||
"prs_created": 2,
|
||||
"prs_merged": 2,
|
||||
"commits_count": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get Project ID** using `repo +info`.
|
||||
2. **Execute** `gitlink-cli api GET /pm/weekly_issues --query 'project_id=<id>'`.
|
||||
3. **Display** weekly report data.
|
||||
|
||||
> [!NOTE]
|
||||
> This is a **Read Operation** — no confirmation needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **工作汇报**:自动生成周工作总结
|
||||
- **进度跟踪**:监控项目一周的进展情况
|
||||
- **团队管理**:了解团队成员的工作贡献
|
||||
- **数据分析**:分析项目发展趋势和效率
|
||||
|
||||
## Weekly Report Structure
|
||||
|
||||
典型周报结构:
|
||||
|
||||
```
|
||||
周报(2026-01-01 至 2026-01-07)
|
||||
|
||||
## 概览统计
|
||||
- 新增 Issue:8 个
|
||||
- 关闭 Issue:5 个
|
||||
- 进行中 Issue:2 个
|
||||
- 合并 PR:4 个
|
||||
- 代码提交:42 次
|
||||
|
||||
## 详细内容
|
||||
### Issue 活动
|
||||
- 新建:8 个 Issue
|
||||
- 完成:5 个 Issue
|
||||
- 持续工作:2 个 Issue
|
||||
|
||||
### Pull Request 活动
|
||||
- 创建:6 个 PR
|
||||
- 合并:4 个 PR
|
||||
|
||||
### 团队贡献
|
||||
- developer1:15 次提交,2 个合并 PR
|
||||
- developer2:12 次提交,1 个合并 PR
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
周报常用操作:
|
||||
|
||||
### 生成简明周报
|
||||
```bash
|
||||
# 生成简明周报摘要
|
||||
gitlink-cli api GET /pm/weekly_issues --query 'project_id=123' --format json | \
|
||||
jq '# 周报摘要
|
||||
"\n## 项目周报 (\(.data.period.start_date) 至 \(.data.period.end_date))",
|
||||
"\n### 统计概览",
|
||||
"- 新增 Issue: \(.data.summary.new_issues) 个",
|
||||
"- 完成 Issue: \(.data.summary.closed_issues) 个",
|
||||
"- 合并 PR: \(.data.summary.merged_prs) 个",
|
||||
"- 代码提交: \(.data.summary.total_commits) 次"'
|
||||
```
|
||||
|
||||
### 分析团队贡献
|
||||
```bash
|
||||
# 按贡献度排序团队成员
|
||||
gitlink-cli api GET /pm/weekly_issues --query 'project_id=123' --format json | \
|
||||
jq '.data.team_contributions | sort_by(.commits_count) | reverse |
|
||||
.[] | "\(.developer): \(.commits_count) 次提交, \(.prs_merged) 个合并 PR"'
|
||||
```
|
||||
|
||||
### 查看活动趋势
|
||||
```bash
|
||||
# 按日期统计活动
|
||||
gitlink-cli api GET /pm/weekly_issues --query 'project_id=123' --format json | \
|
||||
jq '[.data.commits[] | .committed_date | split("T")[0]] |
|
||||
group_by(.) |
|
||||
map({date: .[0], count: length}) |
|
||||
sort_by(.date)'
|
||||
```
|
||||
|
||||
## Report Metrics
|
||||
|
||||
周报关键指标:
|
||||
|
||||
| 指标 | 说明 | 用途 |
|
||||
|------|------|------|
|
||||
| **新增 Issue 数** | 一周内新建的 Issue 数量 | 反映新需求产生速度 |
|
||||
| **关闭 Issue 数** | 一周内关闭的 Issue 数量 | 反映问题解决速度 |
|
||||
| **合并 PR 数** | 一周内合并的 PR 数量 | 反映代码集成速度 |
|
||||
| **提交次数** | 一周内的代码提交次数 | 反映开发活跃度 |
|
||||
| **参与人数** | 有贡献活动的团队成员数 | 反映团队参与度 |
|
||||
|
||||
## Analysis Examples
|
||||
|
||||
周报数据分析示例:
|
||||
|
||||
```bash
|
||||
# 1. 获取项目 ID
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
|
||||
# 2. 查看完整周报
|
||||
gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID" --format json
|
||||
|
||||
# 3. 生成团队贡献排名
|
||||
gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '.data.team_contributions | sort_by(.commits_count) | reverse |
|
||||
|
||||
# 4. 计算完成率
|
||||
gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '(.data.summary.closed_issues / .data.summary.new_issues * 100) |
|
||||
"本周完成率: \(.)%"'
|
||||
|
||||
# 5. 分析代码变更量
|
||||
gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '{additions: [.data.pull_requests[].additions] | add,
|
||||
deletions: [.data.pull_requests[].deletions] | add,
|
||||
net_change: ([.data.pull_requests[].additions] | add) - ([.data.pull_requests[].deletions] | add)}'
|
||||
```
|
||||
|
||||
## Custom Report Generation
|
||||
|
||||
自定义报告生成:
|
||||
|
||||
```bash
|
||||
# 生成 Markdown 格式的周报
|
||||
generate_weekly_report() {
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
END_DATE=$(date +%Y-%m-%d)
|
||||
START_DATE=$(date -d "7 days ago" +%Y-%m-%d)
|
||||
|
||||
echo "# 项目周报 ($START_DATE 至 $END_DATE)"
|
||||
echo ""
|
||||
|
||||
gitlink-cli api GET /pm/weekly_issues \
|
||||
--query "project_id=$PROJECT_ID&start_date=$START_DATE&end_date=$END_DATE" \
|
||||
--format json | \
|
||||
jq -r '
|
||||
"## 概览统计",
|
||||
"- 新增 Issue: \(.data.summary.new_issues) 个",
|
||||
"- 完成 Issue: \(.data.summary.closed_issues) 个",
|
||||
"- 合并 PR: \(.data.summary.merged_prs) 个",
|
||||
"- 代码提交: \(.data.summary.total_commits) 次",
|
||||
"",
|
||||
"## 团队贡献",
|
||||
(.data.team_contributions | sort_by(.commits_count) | reverse |
|
||||
.[] | "- **\(.developer)**: \(.commits_count) 次提交, \(.prs_merged) 个合并 PR"),
|
||||
"",
|
||||
"## 主要完成",
|
||||
(.data.issues[] | select(.status == "closed") |
|
||||
"- [\(.subject)](#issue/\(.id)) - \(.assigned_to)"),
|
||||
"",
|
||||
"## 代码合并",
|
||||
(.data.pull_requests[] | select(.status == "merged") |
|
||||
"- [\(.title)](#pr/\(.id)) - \(.author) (+\(.additions) -\(.deletions))")
|
||||
'
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- 周报数据可以帮助团队了解工作进展
|
||||
- 定期生成周报可以保持项目进度的可视化
|
||||
- 结合其他 PM 数据可以实现完整的项目管理
|
||||
- 使用 JSON 格式输出便于自动化报告生成
|
||||
|
||||
## Integration with Other Features
|
||||
|
||||
与其他功能集成:
|
||||
|
||||
```bash
|
||||
# 周报 + 详细操作
|
||||
# 1. 生成周报概览
|
||||
gitlink-cli api GET /pm/weekly_issues --query 'project_id=123' --format json
|
||||
|
||||
# 2. 查看特定 Issue 详情
|
||||
gitlink-cli issue +view --issue 456
|
||||
|
||||
# 3. 查看特定 PR 详情
|
||||
gitlink-cli pr +view --pr 123
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
周报生成最佳实践:
|
||||
|
||||
1. **定期生成**:每周固定时间生成周报
|
||||
2. **数据验证**:生成后验证数据的准确性
|
||||
3. **格式统一**:使用统一的报告格式
|
||||
4. **趋势分析**:比较不同周报的数据趋势
|
||||
5. **团队分享**:及时分享周报给团队成员
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
高级用法示例:
|
||||
|
||||
```bash
|
||||
# 比较两周的数据
|
||||
compare_weeks() {
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
|
||||
# 本周数据
|
||||
THIS_WEEK=$(gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID" --format json)
|
||||
|
||||
# 上周数据(计算日期)
|
||||
LAST_START=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
LAST_END=$(date -d "8 days ago" +%Y-%m-%d)
|
||||
LAST_WEEK=$(gitlink-cli api GET /pm/weekly_issues \
|
||||
--query "project_id=$PROJECT_ID&start_date=$LAST_START&end_date=$LAST_END" --format json)
|
||||
|
||||
# 比较输出
|
||||
echo "## 周环比分析"
|
||||
echo "新增 Issue: $THIS_WEEK ↓ $LAST_WEEK"
|
||||
echo "完成 Issue: $THIS_WEEK ↓ $LAST_WEEK"
|
||||
}
|
||||
|
||||
# 导出为文件
|
||||
export_weekly_report() {
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
|
||||
gitlink-cli api GET /pm/weekly_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '.' > "weekly_report_$DATE.json"
|
||||
}
|
||||
```
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
团队协作建议:
|
||||
|
||||
1. **定期分享**:每周固定时间分享周报
|
||||
2. **数据透明**:保持团队对项目进度的了解
|
||||
3. **问题讨论**:基于周报数据讨论问题和改进
|
||||
4. **成果认可**:认可和庆祝团队成就
|
||||
5. **持续改进**:基于周报分析优化工作流程
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `400` | PM 模块未开启 | 联系项目管理员开启 PM 模块 |
|
||||
| `404` | 项目不存在 | 检查 project_id 是否正确 |
|
||||
| `400` | 日期格式错误 | 确保日期格式为 YYYY-MM-DD |
|
||||
| `403` | 权限不足 | 确认有查看该项目的权限 |
|
||||
| `404` | 数据不存在 | 指定日期范围内可能没有活动数据 |
|
||||
|
||||
## Report Templates
|
||||
|
||||
报告模板示例:
|
||||
|
||||
```markdown
|
||||
# 项目周报(第{{week_number}}周)
|
||||
|
||||
**时间范围**:{{start_date}} 至 {{end_date}}
|
||||
|
||||
## 📊 核心指标
|
||||
- ✅ 完成 Issue:{{closed_issues}} 个
|
||||
- 🆕 新增 Issue:{{new_issues}} 个
|
||||
- 🔀 合并 PR:{{merged_prs}} 个
|
||||
- 💻 代码提交:{{total_commits}} 次
|
||||
|
||||
## 👥 团队贡献
|
||||
{{#each team_contributions}}
|
||||
### {{developer}}
|
||||
- 提交:{{commits_count}} 次
|
||||
- 合并 PR:{{prs_merged}} 个
|
||||
- 完成 Issue:{{issues_closed}} 个
|
||||
{{/each}}
|
||||
|
||||
## 🎯 主要成果
|
||||
{{#each closed_issues}}
|
||||
- {{subject}} ({{assigned_to}})
|
||||
{{/each}}
|
||||
|
||||
## 🔄 进行中工作
|
||||
{{#each in_progress_issues}}
|
||||
- {{subject}} ({{assigned_to}})
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [pm-kanban](pm-kanban.md) — 看板管理
|
||||
- [pm-sprint](pm-sprint.md) — Sprint 管理
|
||||
- [gitlink-pm](../SKILL.md) — 项目管理总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [repo +info](../../gitlink-repo/references/repo-info.md) — 获取仓库信息
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
# PM Sprint 管理
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **注意**:PM 功能需要项目开启 PM 模块,并通过项目 ID 访问。
|
||||
|
||||
GitLink PM Sprint 功能支持敏捷开发的迭代管理,帮助团队组织和管理特定时间段内的开发任务。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 查看 Sprint Issue 列表
|
||||
gitlink-cli api GET /pm/sprint_issues --query 'project_id=123'
|
||||
|
||||
# 查看当前仓库的项目 ID
|
||||
gitlink-cli repo +info --format json | jq '.data.project_id'
|
||||
|
||||
# 组合命令:自动获取项目 ID 并查看 Sprint
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID"
|
||||
|
||||
# 查看特定 Sprint 的信息
|
||||
gitlink-cli api GET /pm/sprint_issues --query 'project_id=123&sprint_id=1'
|
||||
|
||||
# 查看 Issue 标签
|
||||
gitlink-cli api GET /pm/issue_tags --query 'project_id=123'
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `project_id` | **是** | 项目 ID(通过 `repo +info` 获取) |
|
||||
| `sprint_id` | 否 | Sprint ID(可选,用于查看特定 Sprint) |
|
||||
| `--owner` | 否* | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否* | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET /api/pm/sprint_issues?project_id={project_id}&sprint_id={sprint_id}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"sprint_issues": [
|
||||
{
|
||||
"id": 789,
|
||||
"subject": "完成用户管理模块",
|
||||
"sprint_id": 1,
|
||||
"sprint_name": "Sprint 1 - 基础功能",
|
||||
"status": "open",
|
||||
"priority": "high",
|
||||
"assigned_to": "developer1",
|
||||
"estimated_hours": 40,
|
||||
"spent_hours": 28,
|
||||
"completion_percentage": 70,
|
||||
"start_date": "2026-01-01",
|
||||
"end_date": "2026-01-14",
|
||||
"tags": ["backend", "user-management"]
|
||||
},
|
||||
{
|
||||
"id": 790,
|
||||
"subject": "实现权限控制",
|
||||
"sprint_id": 1,
|
||||
"sprint_name": "Sprint 1 - 基础功能",
|
||||
"status": "in_progress",
|
||||
"priority": "high",
|
||||
"assigned_to": "developer2",
|
||||
"estimated_hours": 32,
|
||||
"spent_hours": 15,
|
||||
"completion_percentage": 47,
|
||||
"start_date": "2026-01-01",
|
||||
"end_date": "2026-01-14",
|
||||
"tags": ["backend", "security"]
|
||||
}
|
||||
],
|
||||
"total_count": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get Project ID** using `repo +info`.
|
||||
2. **Execute** `gitlink-cli api GET /pm/sprint_issues --query 'project_id=<id>'`.
|
||||
3. **Display** sprint issues and progress.
|
||||
|
||||
> [!NOTE]
|
||||
> This is a **Read Operation** — no confirmation needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Sprint 规划**:查看和规划 Sprint 中的任务
|
||||
- **进度跟踪**:监控 Sprint 的执行进度
|
||||
- **资源分配**:合理分配团队成员到 Sprint 任务
|
||||
- **性能分析**:分析团队的开发速度和效率
|
||||
|
||||
## Sprint Lifecycle
|
||||
|
||||
典型的 Sprint 生命周期:
|
||||
|
||||
```
|
||||
1. Sprint 规划
|
||||
├── 确定 Sprint 目标
|
||||
├── 选择要处理的 Issue
|
||||
└── 估算工作量
|
||||
|
||||
2. Sprint 执行
|
||||
├── 开发团队实现功能
|
||||
├── 每日站会同步进度
|
||||
└── 处理阻塞问题
|
||||
|
||||
3. Sprint 评审
|
||||
├── 演示完成的功能
|
||||
├── 收集反馈意见
|
||||
└── 确定验收结果
|
||||
|
||||
4. Sprint 回顾
|
||||
├── 总结经验教训
|
||||
├── 优化工作流程
|
||||
└── 制定改进计划
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
Sprint 常用操作:
|
||||
|
||||
### 查看 Sprint 概览
|
||||
```bash
|
||||
# 查看 Sprint 统计信息
|
||||
gitlink-cli api GET /pm/sprint_issues --query 'project_id=123' --format json | \
|
||||
jq '{total: .data.total_count,
|
||||
high_priority: [.data.sprint_issues[] | select(.priority=="high")] | length,
|
||||
completed: [.data.sprint_issues[] | select(.status=="closed")] | length}'
|
||||
```
|
||||
|
||||
### 查看 Sprint 进度
|
||||
```bash
|
||||
# 计算 Sprint 完成百分比
|
||||
gitlink-cli api GET /pm/sprint_issues --query 'project_id=123' --format json | \
|
||||
jq '[.data.sprint_issues[].completion_percentage] | add / length'
|
||||
```
|
||||
|
||||
### 分析工作负载
|
||||
```bash
|
||||
# 按人员统计工作负载
|
||||
gitlink-cli api GET /pm/sprint_issues --query 'project_id=123' --format json | \
|
||||
jq '[.data.sprint_issues[] | {assigned_to: .assigned_to, estimated: .estimated_hours}] |
|
||||
group_by(.assigned_to) |
|
||||
map({developer: .[0].assigned_to, total_hours: (map(.estimated) | add)})'
|
||||
```
|
||||
|
||||
## Sprint Metrics
|
||||
|
||||
Sprint 关键指标:
|
||||
|
||||
| 指标 | 说明 | 计算方式 |
|
||||
|------|------|----------|
|
||||
| **Sprint 速度** | 团队在一个 Sprint 中完成的工作量 | 完成的 Issue 数 × 复杂度权重 |
|
||||
| **完成率** | Sprint 中已完成任务的百分比 | 已完成数 / 总数 × 100% |
|
||||
| **剩余工作量** | Sprint 中未完成的工作量 | 未完成任务的估算小时数 |
|
||||
| **工作负载** | 团队成员的工作分布 | 每人分配的估算小时数 |
|
||||
| **延期风险** | 可能无法按时完成的任务 | 接近截止日期但未完成的任务 |
|
||||
|
||||
## Analysis Examples
|
||||
|
||||
Sprint 数据分析示例:
|
||||
|
||||
```bash
|
||||
# 1. 获取项目 ID
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
|
||||
# 2. 查看 Sprint 概览
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json
|
||||
|
||||
# 3. 分析高优先级任务
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '.data.sprint_issues[] | select(.priority=="high") | {subject, status, completion_percentage}'
|
||||
|
||||
# 4. 识别延期风险
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '.data.sprint_issues[] |
|
||||
select(.status != "closed" and .end_date < (now | todate)) |
|
||||
{subject, end_date, completion_percentage}'
|
||||
|
||||
# 5. 计算团队效率
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '{total_estimated: [.data.sprint_issues[].estimated_hours] | add,
|
||||
total_spent: [.data.sprint_issues[].spent_hours] | add,
|
||||
efficiency: ([.data.sprint_issues[].spent_hours] | add) /
|
||||
([.data.sprint_issues[].estimated_hours] | add) * 100}'
|
||||
```
|
||||
|
||||
## Sprint Planning
|
||||
|
||||
Sprint 规划建议:
|
||||
|
||||
1. **合理估算**:基于历史数据估算工作量
|
||||
2. **优先级排序**:优先处理高价值和高优先级任务
|
||||
3. **负载均衡**:合理分配任务给团队成员
|
||||
4. **预留缓冲**:为不可预见的问题预留时间
|
||||
|
||||
## Tips
|
||||
|
||||
- Sprint 数据可以帮助团队了解开发进度
|
||||
- 定期查看 Sprint 统计可以及时发现问题
|
||||
- 结合 Issue 操作可以实现完整的任务管理
|
||||
- 使用 JSON 格式输出便于自动化分析
|
||||
|
||||
## Integration with Other Features
|
||||
|
||||
与其他功能集成:
|
||||
|
||||
```bash
|
||||
# Sprint + Issue 操作
|
||||
# 1. 查看 Sprint 中的任务
|
||||
gitlink-cli api GET /pm/sprint_issues --query 'project_id=123' --format json
|
||||
|
||||
# 2. 查看特定任务详情
|
||||
gitlink-cli issue +view --issue 789
|
||||
|
||||
# 3. 更新任务状态
|
||||
gitlink-cli issue +update --issue 789 --status_id 3 --done_ratio 80
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
Sprint 管理最佳实践:
|
||||
|
||||
1. **时间盒固定**:Sprint 时长通常为 2-4 周
|
||||
2. **目标明确**:每个 Sprint 应有明确的目标
|
||||
3. **任务可衡量**:Sprint 任务应该是可衡量和可完成的
|
||||
4. **定期回顾**:每个 Sprint 结束后进行回顾总结
|
||||
5. **持续改进**:基于回顾结果优化工作流程
|
||||
|
||||
## Error Handling
|
||||
|
||||
常见错误及解决方案:
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `400` | PM 模块未开启 | 联系项目管理员开启 PM 模块 |
|
||||
| `404` | 项目不存在 | 检查 project_id 是否正确 |
|
||||
| `404` | Sprint 不存在 | 检查 sprint_id 是否正确 |
|
||||
| `403` | 权限不足 | 确认有查看该项目的权限 |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
高级用法示例:
|
||||
|
||||
```bash
|
||||
# 生成 Sprint 报告
|
||||
generate_sprint_report() {
|
||||
PROJECT_ID=$(gitlink-cli repo +info --format json | jq -r '.data.project_id')
|
||||
|
||||
echo "# Sprint Report"
|
||||
echo "## Overview"
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq -r '"Total: \(.data.total_count) issues"'
|
||||
|
||||
echo "## Priority Distribution"
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '.data.sprint_issues | group_by(.priority) | map({priority: .[0].priority, count: length})'
|
||||
|
||||
echo "## Team Workload"
|
||||
gitlink-cli api GET /pm/sprint_issues --query "project_id=$PROJECT_ID" --format json | \
|
||||
jq '[.data.sprint_issues[] | {assigned_to: .assigned_to, hours: .estimated_hours}] |
|
||||
group_by(.assigned_to) |
|
||||
map({developer: .[0].assigned_to, total_hours: (map(.hours) | add)})'
|
||||
}
|
||||
```
|
||||
|
||||
## Team Collaboration
|
||||
|
||||
团队协作建议:
|
||||
|
||||
1. **Sprint 规划会议**:全团队参与 Sprint 规划
|
||||
2. **每日站会**:简短同步进度和问题
|
||||
3. **Sprint 评审**:演示和验收完成的功能
|
||||
4. **Sprint 回顾**:总结经验,持续改进
|
||||
|
||||
## References
|
||||
|
||||
- [pm-kanban](pm-kanban.md) — 看板管理
|
||||
- [pm-report](pm-report.md) — 周报生成
|
||||
- [gitlink-pm](../SKILL.md) — 项目管理总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [repo +info](../../gitlink-repo/references/repo-info.md) — 获取仓库信息
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
---
|
||||
name: gitlink-webhook
|
||||
version: 1.0.0
|
||||
description: "Webhook 管理:创建、查看、更新、删除、测试 Webhook,配置自动化触发器。当用户需要配置 GitLink 仓库的 Webhook 自动化通知时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli webhook --help"
|
||||
---
|
||||
|
||||
# gitlink-webhook(Webhook 操作)
|
||||
|
||||
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
|
||||
**CRITICAL — 所有 Shortcuts 在执行写入/删除操作前,务必先确认用户意图。**
|
||||
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。**
|
||||
|
||||
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | 说明 | 需要认证 |
|
||||
|----------|------|----------|
|
||||
| `webhook +list` | 列出仓库的所有 Webhook | 是 |
|
||||
| `webhook +create` | 创建新 Webhook | 是 |
|
||||
| `webhook +update` | 更新 Webhook 配置 | 是 |
|
||||
| `webhook +delete` | 删除 Webhook | 是 |
|
||||
| `webhook +test` | 测试 Webhook 推送(发送 ping 事件) | 是 |
|
||||
| `webhook +info` | 查看 Webhook 详情 | 是 |
|
||||
| `webhook +events` | 列出所有支持的事件类型 | 否 |
|
||||
|
||||
## 支持的事件类型
|
||||
|
||||
| 事件类型 | 说明 | 触发时机 |
|
||||
|----------|------|----------|
|
||||
| `push` | 代码推送事件 | 向仓库推送代码时 |
|
||||
| `pull_request` | Pull 请求事件 | 创建、更新、关闭 PR 时 |
|
||||
| `issue` | Issue 事件 | 创建、更新、关闭 Issue 时 |
|
||||
| `issue_assign` | Issue 指派事件 | Issue 被指派给用户时 |
|
||||
| `issue_comment` | Issue 评论事件 | Issue 添加评论时 |
|
||||
| `pull_request_assign` | PR 指派事件 | PR 被指派给审查者时 |
|
||||
| `pull_request_comment` | PR 评论事件 | PR 添加评论时 |
|
||||
| `merge_request` | 合并请求事件 | PR 被合并时 |
|
||||
| `repository` | 仓库事件 | 仓库设置变更时 |
|
||||
| `branch` | 分支事件 | 创建或删除分支时 |
|
||||
| `tag` | 标签事件 | 创建或删除标签时 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本操作
|
||||
|
||||
```bash
|
||||
# 列出仓库的所有 Webhook
|
||||
gitlink-cli webhook +list --owner myuser --repo myrepo
|
||||
|
||||
# 查看 Webhook 详情
|
||||
gitlink-cli webhook +info --owner myuser --repo myrepo --id 123
|
||||
|
||||
# 列出所有支持的事件类型
|
||||
gitlink-cli webhook +events
|
||||
```
|
||||
|
||||
### 创建 Webhook
|
||||
|
||||
```bash
|
||||
# 创建基本的 Webhook(仅监听 push 事件)
|
||||
gitlink-cli webhook +create --owner myuser --repo myrepo --url https://example.com/webhook
|
||||
|
||||
# 创建多事件 Webhook
|
||||
gitlink-cli webhook +create --owner myuser --repo myrepo --url https://example.com/webhook --events push,pull_request,issue
|
||||
|
||||
# 创建带密钥的 Webhook
|
||||
gitlink-cli webhook +create --owner myuser --repo myrepo --url https://example.com/webhook --secret my-secret-key --events push
|
||||
|
||||
# 创建带描述的 Webhook
|
||||
gitlink-cli webhook +create --owner myuser --repo myrepo --url https://example.com/webhook --description "CI/CD automation"
|
||||
```
|
||||
|
||||
### 更新 Webhook
|
||||
|
||||
```bash
|
||||
# 更新 Webhook URL
|
||||
gitlink-cli webhook +update --owner myuser --repo myrepo --id 123 --url https://new-url.com/webhook
|
||||
|
||||
# 更新监听事件
|
||||
gitlink-cli webhook +update --owner myuser --repo myrepo --id 123 --events push,pull_request
|
||||
|
||||
# 激活/停用 Webhook
|
||||
gitlink-cli webhook +update --owner myuser --repo myrepo --id 123 --active false
|
||||
|
||||
# 更新多个属性
|
||||
gitlink-cli webhook +update --owner myuser --repo myrepo --id 123 --url https://new-url.com/webhook --events push,pull_request --secret new-secret
|
||||
```
|
||||
|
||||
### 测试和删除
|
||||
|
||||
```bash
|
||||
# 测试 Webhook(发送 ping 事件)
|
||||
gitlink-cli webhook +test --owner myuser --repo myrepo --id 123
|
||||
|
||||
# 测试特定事件类型
|
||||
gitlink-cli webhook +test --owner myuser --repo myrepo --id 123 --event push
|
||||
|
||||
# 删除 Webhook
|
||||
gitlink-cli webhook +delete --owner myuser --repo myrepo --id 123
|
||||
```
|
||||
|
||||
## 典型使用场景
|
||||
|
||||
### 场景1: 配置 CI/CD 自动化
|
||||
|
||||
```bash
|
||||
# 为 CI/CD 系统创建 Webhook
|
||||
gitlink-cli webhook +create \
|
||||
--owner myuser --repo myrepo \
|
||||
--url https://ci.example.com/gitlink/webhook \
|
||||
--events push,pull_request \
|
||||
--secret ci-secret-key \
|
||||
--description "Trigger CI/CD pipeline"
|
||||
```
|
||||
|
||||
### 场景2: 配置 Issue 通知
|
||||
|
||||
```bash
|
||||
# 创建 Issue 通知 Webhook
|
||||
gitlink-cli webhook +create \
|
||||
--owner myuser --repo myrepo \
|
||||
--url https://notification.example.com/issues \
|
||||
--events issue,issue_comment,issue_assign \
|
||||
--description "Issue notifications"
|
||||
```
|
||||
|
||||
### 场景3: 配置 PR 审查通知
|
||||
|
||||
```bash
|
||||
# 创建 PR 审查 Webhook
|
||||
gitlink-cli webhook +create \
|
||||
--owner myuser --repo myrepo \
|
||||
--url https://review.example.com/prs \
|
||||
--events pull_request,pull_request_comment,pull_request_assign \
|
||||
--description "PR review notifications"
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. 认证错误
|
||||
```bash
|
||||
Error: [401] Authentication failed
|
||||
```
|
||||
**解决方案**: 运行 `gitlink-cli auth login` 重新认证
|
||||
|
||||
#### 2. 权限不足
|
||||
```bash
|
||||
Error: [403] You are not authorized to manage webhooks
|
||||
```
|
||||
**解决方案**: 确认您对仓库有管理员权限
|
||||
|
||||
#### 3. 无效的事件类型
|
||||
```bash
|
||||
Error: no valid events specified
|
||||
```
|
||||
**解决方案**: 使用 `gitlink-cli webhook +events` 查看支持的事件类型
|
||||
|
||||
#### 4. Webhook 不存在
|
||||
```bash
|
||||
Error: [404] Webhook not found
|
||||
```
|
||||
**解决方案**: 使用 `gitlink-cli webhook +list` 确认 Webhook ID 是否正确
|
||||
|
||||
## AI Agent 使用指南
|
||||
|
||||
### 检查现有 Webhook
|
||||
```bash
|
||||
# 1. 列出所有 Webhook
|
||||
gitlink-cli webhook +list --owner $OWNER --repo $REPO --format json
|
||||
|
||||
# 2. 检查是否有特定类型的 Webhook
|
||||
gitlink-cli webhook +list --owner $OWNER --repo $REPO --format json | jq '.data.webhooks[] | select(.hook_url | contains("ci-system"))'
|
||||
```
|
||||
|
||||
### 创建 Webhook 的最佳实践
|
||||
```bash
|
||||
# 1. 先查看支持的事件
|
||||
gitlink-cli webhook +events
|
||||
|
||||
# 2. 创建 Webhook 并验证
|
||||
gitlink-cli webhook +create --owner $OWNER --repo $REPO --url $URL --events $EVENTS
|
||||
|
||||
# 3. 测试 Webhook 是否正常工作
|
||||
gitlink-cli webhook +test --owner $OWNER --repo $REPO --id $WEBHOOK_ID
|
||||
```
|
||||
|
||||
### 安全建议
|
||||
- **使用密钥**: 为 Webhook 设置密钥以验证请求来源
|
||||
- **HTTPS**: 始终使用 HTTPS URL 作为 Webhook 回调地址
|
||||
- **最小权限**: 只监听必要的事件类型
|
||||
- **定期轮换**: 定期更新 Webhook 密钥
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [`webhook-list.md`](references/webhook-list.md) - 列出 Webhook 详细说明
|
||||
- [`webhook-create.md`](references/webhook-create.md) - 创建 Webhook 详细说明
|
||||
- [`webhook-update.md`](references/webhook-update.md) - 更新 Webhook 详细说明
|
||||
- [`webhook-delete.md`](references/webhook-delete.md) - 删除 Webhook 详细说明
|
||||
- [`webhook-test.md`](references/webhook-test.md) - 测试 Webhook 详细说明
|
||||
- [`webhook-info.md`](references/webhook-info.md) - 查看 Webhook 详细说明
|
||||
- [`examples/webhook-workflow.md`](examples/webhook-workflow.md) - 完整工作流示例
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API 限制**: GitLink 对 Webhook 数量有限制,通常每个仓库不超过 20 个
|
||||
2. **URL 要求**: Webhook URL 必须是公网可访问的 HTTPS 地址
|
||||
3. **超时设置**: Webhook 请求超时时间为 10 秒
|
||||
4. **重试机制**: GitLink 会在失败时重试 3 次,间隔分别为 1s、5s、10s
|
||||
5. **事件顺序**: 同一事件的多个 Webhook 按创建顺序依次触发
|
||||
6. **测试限制**: 测试 Webhook 不会触发实际的业务逻辑,仅验证连通性
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Webhook 未触发
|
||||
1. 检查 Webhook 是否激活:`gitlink-cli webhook +info --id <id> --active true`
|
||||
2. 验证事件类型是否正确:`gitlink-cli webhook +info --id <id>`
|
||||
3. 测试 Webhook 连通性:`gitlink-cli webhook +test --id <id>`
|
||||
|
||||
### Webhook 响应异常
|
||||
1. 检查回调服务器是否正常运行
|
||||
2. 验证 Webhook URL 是否可访问
|
||||
3. 查看 GitLink 服务器日志确认请求是否发送
|
||||
|
||||
### 权限问题
|
||||
1. 确认当前用户是仓库管理员或所有者
|
||||
2. 检查 Token 是否有足够权限:`gitlink-cli auth status`
|
||||
|
|
@ -0,0 +1,694 @@
|
|||
# Webhook 完整工作流示例
|
||||
|
||||
本文档提供了 GitLink Webhook 的完整使用场景和最佳实践示例。
|
||||
|
||||
## 目录
|
||||
|
||||
- [场景1: CI/CD 自动化](#场景1-cicd-自动化)
|
||||
- [场景2: Issue 和 PR 通知](#场景2-issue-和-pr-通知)
|
||||
- [场景3: 多环境部署](#场景3-多环境部署)
|
||||
- [场景4: Webhook 迁移](#场景4-webhook-迁移)
|
||||
- [场景5: 故障排查](#场景5-故障排查)
|
||||
- [场景6: 安全最佳实践](#场景6-安全最佳实践)
|
||||
|
||||
---
|
||||
|
||||
## 场景1: CI/CD 自动化
|
||||
|
||||
### 目标
|
||||
为 Jenkins CI/CD 系统配置 Webhook,实现代码推送时自动触发构建。
|
||||
|
||||
### 完整流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# cicd-webhook-setup.sh
|
||||
|
||||
PROJECT_OWNER="mycompany"
|
||||
PROJECT_REPO="main-app"
|
||||
JENKINS_URL="https://jenkins.example.com/gitlink-webhook"
|
||||
WEBHOOK_SECRET="jenkins-secret-key-2024"
|
||||
|
||||
echo "=== Setting up CI/CD Webhook for $PROJECT_OWNER/$PROJECT_REPO ==="
|
||||
|
||||
# 1. 检查是否已存在 CI/CD Webhook
|
||||
echo "1. Checking existing webhooks..."
|
||||
existing=$(gitlink-cli webhook +list \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--format json | \
|
||||
jq -r ".data.webhooks[] | select(.hook_url | contains(\"jenkins\")) | .id")
|
||||
|
||||
if [ -n "$existing" ]; then
|
||||
echo "Found existing CI/CD webhook: $existing"
|
||||
read -p "Delete existing webhook? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
gitlink-cli webhook +delete --owner $PROJECT_OWNER --repo $PROJECT_REPO --id $existing
|
||||
echo "Existing webhook deleted"
|
||||
else
|
||||
echo "Aborting setup"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. 创建新的 Webhook
|
||||
echo "2. Creating new CI/CD webhook..."
|
||||
WEBHOOK_INFO=$(gitlink-cli webhook +create \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--url "$JENKINS_URL" \
|
||||
--events push,pull_request \
|
||||
--secret "$WEBHOOK_SECRET" \
|
||||
--description "Jenkins CI/CD automation" \
|
||||
--format json)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
WEBHOOK_ID=$(echo $WEBHOOK_INFO | jq -r '.data.id')
|
||||
echo "✓ Webhook created successfully: $WEBHOOK_ID"
|
||||
else
|
||||
echo "✗ Failed to create webhook"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. 测试 Webhook
|
||||
echo "3. Testing webhook..."
|
||||
if gitlink-cli webhook +test --owner $PROJECT_OWNER --repo $PROJECT_REPO --id $WEBHOOK_ID; then
|
||||
echo "✓ Webhook test successful"
|
||||
else
|
||||
echo "⚠ Webhook test failed, please check Jenkins server"
|
||||
read -p "Continue anyway? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
gitlink-cli webhook +delete --id $WEBHOOK_ID
|
||||
echo "Webhook deleted due to test failure"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. 验证配置
|
||||
echo "4. Verifying configuration..."
|
||||
gitlink-cli webhook +info --owner $PROJECT_OWNER --repo $PROJECT_REPO --id $WEBHOOK_ID
|
||||
|
||||
echo "=== CI/CD Webhook Setup Complete ==="
|
||||
echo "Webhook ID: $WEBHOOK_ID"
|
||||
echo "Jenkins URL: $JENKINS_URL"
|
||||
echo "Events: push, pull_request"
|
||||
```
|
||||
|
||||
### 使用说明
|
||||
|
||||
```bash
|
||||
# 1. 设置脚本权限
|
||||
chmod +x cicd-webhook-setup.sh
|
||||
|
||||
# 2. 运行脚本
|
||||
./cicd-webhook-setup.sh
|
||||
|
||||
# 3. 验证 Webhook 是否正常工作
|
||||
# 在 Jenkins 中检查是否收到 Webhook 事件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景2: Issue 和 PR 通知
|
||||
|
||||
### 目标
|
||||
配置 Slack 通知,在 Issue 和 PR 活动时发送消息到团队频道。
|
||||
|
||||
### 完整流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# notification-webhook-setup.sh
|
||||
|
||||
PROJECT_OWNER="myteam"
|
||||
PROJECT_REPO="project-x"
|
||||
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
||||
|
||||
echo "=== Setting up Notification Webhooks ==="
|
||||
|
||||
# Issue 通知 Webhook
|
||||
echo "1. Creating Issue notification webhook..."
|
||||
ISSUE_WEBHOOK_ID=$(gitlink-cli webhook +create \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--url "$SLACK_WEBHOOK_URL" \
|
||||
--events issue,issue_comment,issue_assign \
|
||||
--description "Issue notifications to #dev-team" \
|
||||
--format json | jq -r '.data.id')
|
||||
|
||||
echo "✓ Issue webhook created: $ISSUE_WEBHOOK_ID"
|
||||
|
||||
# PR 通知 Webhook
|
||||
echo "2. Creating PR notification webhook..."
|
||||
PR_WEBHOOK_ID=$(gitlink-cli webhook +create \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--url "$SLACK_WEBHOOK_URL" \
|
||||
--events pull_request,pull_request_comment,pull_request_assign \
|
||||
--description "PR notifications to #dev-team" \
|
||||
--format json | jq -r '.data.id')
|
||||
|
||||
echo "✓ PR webhook created: $PR_WEBHOOK_ID"
|
||||
|
||||
# 测试两个 Webhook
|
||||
echo "3. Testing webhooks..."
|
||||
gitlink-cli webhook +test --id $ISSUE_WEBHOOK_ID --event issue
|
||||
gitlink-cli webhook +test --id $PR_WEBHOOK_ID --event pull_request
|
||||
|
||||
# 查看配置
|
||||
echo "4. Webhook summary:"
|
||||
echo "Issue Webhook: $ISSUE_WEBHOOK_ID"
|
||||
gitlink-cli webhook +info --id $ISSUE_WEBHOOK_ID
|
||||
echo
|
||||
echo "PR Webhook: $PR_WEBHOOK_ID"
|
||||
gitlink-cli webhook +info --id $PR_WEBHOOK_ID
|
||||
|
||||
echo "=== Notification Setup Complete ==="
|
||||
```
|
||||
|
||||
### 多团队通知
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# multi-team-notifications.sh
|
||||
|
||||
# 为不同团队配置不同的通知
|
||||
declare -A TEAM_WEBHOOKS=(
|
||||
["dev-team"]="https://hooks.slack.com/services/DEV/TEAM/WEBHOOK"
|
||||
["ops-team"]="https://hooks.slack.com/services/OPS/TEAM/WEBHOOK"
|
||||
["security-team"]="https://hooks.slack.com/services/SECURITY/TEAM/WEBHOOK"
|
||||
)
|
||||
|
||||
for team in "${!TEAM_WEBHOOKS[@]}"; do
|
||||
webhook_url="${TEAM_WEBHOOKS[$team]}"
|
||||
|
||||
echo "Setting up webhook for $team..."
|
||||
|
||||
gitlink-cli webhook +create \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--url "$webhook_url" \
|
||||
--events push,pull_request,issue \
|
||||
--description "Notifications for #$team"
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景3: 多环境部署
|
||||
|
||||
### 目标
|
||||
为不同环境(开发、测试、生产)配置独立的 Webhook。
|
||||
|
||||
### 完整流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# multi-env-webhook-setup.sh
|
||||
|
||||
PROJECT_OWNER="mycompany"
|
||||
PROJECT_REPO="main-app"
|
||||
|
||||
# 环境配置
|
||||
declare -A ENVIRONMENTS=(
|
||||
["development"]="https://ci-dev.example.com/webhook"
|
||||
["testing"]="https://ci-test.example.com/webhook"
|
||||
["production"]="https:ci-prod.example.com/webhook"
|
||||
)
|
||||
|
||||
# 为每个环境创建 Webhook
|
||||
for env in "${!ENVIRONMENTS[@]}"; do
|
||||
webhook_url="${ENVIRONMENTS[$env]}"
|
||||
secret="${env}-secret-$(date +%Y%m%d)"
|
||||
|
||||
echo "=== Setting up $env environment webhook ==="
|
||||
|
||||
# 创建 Webhook
|
||||
webhook_id=$(gitlink-cli webhook +create \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--url "$webhook_url" \
|
||||
--events push,pull_request \
|
||||
--secret "$secret" \
|
||||
--description "$env environment CI/CD" \
|
||||
--format json | jq -r '.data.id')
|
||||
|
||||
echo "✓ $env webhook created: $webhook_id"
|
||||
|
||||
# 根据环境设置不同的激活状态
|
||||
if [ "$env" = "production" ]; then
|
||||
# 生产环境默认激活
|
||||
echo "Production webhook is active"
|
||||
else
|
||||
# 其他环境暂时停用,需要时手动激活
|
||||
gitlink-cli webhook +update --id $webhook_id --active false
|
||||
echo "$env webhook created but inactive (activate manually when needed)"
|
||||
fi
|
||||
|
||||
echo
|
||||
done
|
||||
|
||||
echo "=== Multi-environment setup complete ==="
|
||||
echo "Review created webhooks:"
|
||||
gitlink-cli webhook +list
|
||||
```
|
||||
|
||||
### 环境切换
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# switch-active-environment.sh
|
||||
|
||||
# 切换激活的环境
|
||||
TARGET_ENV=$1
|
||||
|
||||
if [ -z "$TARGET_ENV" ]; then
|
||||
echo "Usage: $0 <environment>"
|
||||
echo "Available environments: development, testing, production"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Switching to $TARGET_ENV environment ==="
|
||||
|
||||
# 停用所有环境 Webhook
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
webhook_url=$(gitlink-cli webhook +info --id $id --format json | jq -r '.data.hook_url')
|
||||
|
||||
if [[ "$webhook_url" == *"ci-"* ]]; then
|
||||
echo "Deactivating webhook $id..."
|
||||
gitlink-cli webhook +update --id $id --active false
|
||||
fi
|
||||
done
|
||||
|
||||
# 激活目标环境 Webhook
|
||||
target_webhook_id=$(gitlink-cli webhook +list --format json | \
|
||||
jq -r ".data.webhooks[] | select(.hook_url | contains(\"$TARGET_ENV\")) | .id")
|
||||
|
||||
if [ -n "$target_webhook_id" ]; then
|
||||
echo "Activating $TARGET_ENV webhook: $target_webhook_id"
|
||||
gitlink-cli webhook +update --id $target_webhook_id --active true
|
||||
|
||||
# 测试激活的 Webhook
|
||||
gitlink-cli webhook +test --id $target_webhook_id
|
||||
|
||||
echo "✓ Switched to $TARGET_ENV environment"
|
||||
else
|
||||
echo "✗ No webhook found for $TARGET_ENV environment"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景4: Webhook 迁移
|
||||
|
||||
### 目标
|
||||
将 Webhook 从旧服务器迁移到新服务器。
|
||||
|
||||
### 完整流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# webhook-migration.sh
|
||||
|
||||
OLD_SERVER="old-ci.example.com"
|
||||
NEW_SERVER="new-ci.example.com"
|
||||
PROJECT_OWNER="mycompany"
|
||||
PROJECT_REPO="main-app"
|
||||
|
||||
echo "=== Webhook Migration: $OLD_SERVER → $NEW_SERVER ==="
|
||||
|
||||
# 1. 查找需要迁移的 Webhook
|
||||
echo "1. Finding webhooks to migrate..."
|
||||
webhooks_to_migrate=$(gitlink-cli webhook +list \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--format json | \
|
||||
jq -r ".data.webhooks[] | select(.hook_url | contains(\"$OLD_SERVER\"))")
|
||||
|
||||
webhook_count=$(echo "$webhooks_to_migrate" | jq -r '. | length')
|
||||
|
||||
if [ "$webhook_count" -eq 0 ]; then
|
||||
echo "No webhooks found for $OLD_SERVER"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $webhook_count webhook(s) to migrate"
|
||||
|
||||
# 2. 为每个 Webhook 创建迁移记录
|
||||
echo "$webhooks_to_migrate" | jq -c '.[]' | while read -r webhook; do
|
||||
old_id=$(echo $webhook | jq -r '.id')
|
||||
old_url=$(echo $webhook | jq -r '.hook_url')
|
||||
events=$(echo $webhook | jq -r '.events | join(",")')
|
||||
description=$(echo $webhook | jq -r '.description')
|
||||
|
||||
# 生成新 URL
|
||||
new_url=$(echo $old_url | sed "s/$OLD_SERVER/$NEW_SERVER/g")
|
||||
|
||||
echo "=== Migrating webhook $old_id ==="
|
||||
echo "Old URL: $old_url"
|
||||
echo "New URL: $new_url"
|
||||
echo "Events: $events"
|
||||
|
||||
# 创建新 Webhook
|
||||
echo "Creating new webhook..."
|
||||
new_id=$(gitlink-cli webhook +create \
|
||||
--owner $PROJECT_OWNER \
|
||||
--repo $PROJECT_REPO \
|
||||
--url "$new_url" \
|
||||
--events "$events" \
|
||||
--description "$description (migrated)" \
|
||||
--format json | jq -r '.data.id')
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ New webhook created: $new_id"
|
||||
|
||||
# 测试新 Webhook
|
||||
echo "Testing new webhook..."
|
||||
if gitlink-cli webhook +test --id $new_id; then
|
||||
echo "✓ New webhook test successful"
|
||||
|
||||
# 备份旧 Webhook 配置
|
||||
echo "$webhook" > "webhook_backup_${old_id}.json"
|
||||
|
||||
# 删除旧 Webhook
|
||||
echo "Deleting old webhook: $old_id"
|
||||
gitlink-cli webhook +delete --id $old_id
|
||||
|
||||
echo "✓ Migration complete for webhook $old_id"
|
||||
else
|
||||
echo "⚠ New webhook test failed, keeping old webhook"
|
||||
gitlink-cli webhook +delete --id $new_id
|
||||
fi
|
||||
else
|
||||
echo "✗ Failed to create new webhook"
|
||||
fi
|
||||
|
||||
echo
|
||||
done
|
||||
|
||||
echo "=== Migration Complete ==="
|
||||
echo "Current webhooks:"
|
||||
gitlink-cli webhook +list --owner $PROJECT_OWNER --repo $PROJECT_REPO
|
||||
```
|
||||
|
||||
### 回滚迁移
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# rollback-migration.sh
|
||||
|
||||
echo "=== Webhook Migration Rollback ==="
|
||||
|
||||
# 从备份文件恢复 Webhook
|
||||
for backup_file in webhook_backup_*.json; do
|
||||
old_id=$(echo $backup_file | sed 's/webhook_backup_\([0-9]*\)\.json/\1/')
|
||||
|
||||
echo "Restoring webhook: $old_id"
|
||||
|
||||
# 读取备份配置
|
||||
webhook_config=$(cat "$backup_file")
|
||||
old_url=$(echo $webhook_config | jq -r '.hook_url')
|
||||
events=$(echo $webhook_config | jq -r '.events | join(",")')
|
||||
description=$(echo $webhook_config | jq -r '.description')
|
||||
|
||||
# 重新创建 Webhook
|
||||
restored_id=$(gitlink-cli webhook +create \
|
||||
--url "$old_url" \
|
||||
--events "$events" \
|
||||
--description "$description (restored)" \
|
||||
--format json | jq -r '.data.id')
|
||||
|
||||
echo "✓ Webhook restored: $restored_id"
|
||||
done
|
||||
|
||||
echo "=== Rollback Complete ==="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景5: 故障排查
|
||||
|
||||
### 目标
|
||||
诊断和修复 Webhook 问题。
|
||||
|
||||
### 故障排查脚本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# webhook-troubleshooting.sh
|
||||
|
||||
WEBHOOK_ID=$1
|
||||
|
||||
if [ -z "$WEBHOOK_ID" ]; then
|
||||
echo "Usage: $0 <webhook_id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Webhook Troubleshooting for ID: $WEBHOOK_ID ==="
|
||||
echo
|
||||
|
||||
# 1. 检查 Webhook 是否存在
|
||||
echo "1. Checking webhook existence..."
|
||||
if ! gitlink-cli webhook +info --id $WEBHOOK_ID >/dev/null 2>&1; then
|
||||
echo "✗ Webhook not found"
|
||||
echo "Available webhooks:"
|
||||
gitlink-cli webhook +list
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Webhook exists"
|
||||
|
||||
# 2. 获取 Webhook 详细信息
|
||||
echo "2. Webhook configuration:"
|
||||
webhook_info=$(gitlink-cli webhook +info --id $WEBHOOK_ID --format json)
|
||||
echo "$webhook_info" | jq -r '.data | {
|
||||
URL: .hook_url,
|
||||
Active: .is_active,
|
||||
Events: .events | join(", "),
|
||||
"Last Delivery": .last_delivery.timestamp,
|
||||
"Success Rate": (.delivery_statistics.success_rate // "N/A")
|
||||
}'
|
||||
|
||||
# 3. 检查 Webhook 是否激活
|
||||
is_active=$(echo $webhook_info | jq -r '.data.is_active')
|
||||
if [ "$is_active" != "true" ]; then
|
||||
echo "⚠ Webhook is not active"
|
||||
read -p "Activate webhook now? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --active true
|
||||
echo "✓ Webhook activated"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. 测试网络连通性
|
||||
echo "3. Testing network connectivity..."
|
||||
webhook_url=$(echo $webhook_info | jq -r '.data.hook_url')
|
||||
if curl -s -o /dev/null -w "%{http_code}" "$webhook_url" | grep -q "200\|301\|302"; then
|
||||
echo "✓ URL is accessible (HTTP $(curl -s -o /dev/null -w "%{http_code}" "$webhook_url"))"
|
||||
else
|
||||
echo "✗ URL is not accessible"
|
||||
echo "Testing with curl:"
|
||||
curl -v "$webhook_url" 2>&1 | head -20
|
||||
fi
|
||||
|
||||
# 5. 测试 Webhook
|
||||
echo "4. Testing webhook delivery..."
|
||||
if gitlink-cli webhook +test --id $WEBHOOK_ID; then
|
||||
echo "✓ Webhook test successful"
|
||||
else
|
||||
echo "✗ Webhook test failed"
|
||||
echo "Common issues:"
|
||||
echo " - URL is not reachable"
|
||||
echo " - Server is not responding"
|
||||
echo " - Firewall blocking requests"
|
||||
echo " - SSL certificate issues"
|
||||
fi
|
||||
|
||||
# 6. 检查成功率
|
||||
echo "5. Checking delivery statistics..."
|
||||
success_rate=$(echo $webhook_info | jq -r '.data.delivery_statistics.success_rate // "N/A"')
|
||||
if [ "$success_rate" != "N/A" ]; then
|
||||
if (( $(echo "$success_rate < 90" | bc -l) )); then
|
||||
echo "⚠ Low success rate: $success_rate%"
|
||||
echo "Recommendation: Check webhook server logs for errors"
|
||||
else
|
||||
echo "✓ Good success rate: $success_rate%"
|
||||
fi
|
||||
else
|
||||
echo "No delivery statistics available (webhook may be new)"
|
||||
fi
|
||||
|
||||
# 7. 诊断建议
|
||||
echo "6. Troubleshooting recommendations:"
|
||||
echo " - Check webhook server logs: tail -f /var/log/webhook-server.log"
|
||||
echo " - Test webhook URL manually: curl -X POST $webhook_url"
|
||||
echo " - Verify SSL certificate: openssl s_client -connect $(echo $webhook_url | sed 's/https:\/\///' | sed 's/:443//')"
|
||||
|
||||
echo "=== Troubleshooting Complete ==="
|
||||
```
|
||||
|
||||
### 常见问题解决
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# common-webhook-fixes.sh
|
||||
|
||||
# 问题1: Webhook 未触发
|
||||
fix_inactive_webhook() {
|
||||
WEBHOOK_ID=$1
|
||||
echo "Fixing inactive webhook: $WEBHOOK_ID"
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --active true
|
||||
gitlink-cli webhook +test --id $WEBHOOK_ID
|
||||
}
|
||||
|
||||
# 问题2: URL 配置错误
|
||||
fix_webhook_url() {
|
||||
WEBHOOK_ID=$1
|
||||
CORRECT_URL=$2
|
||||
echo "Fixing webhook URL for: $WEBHOOK_ID"
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --url "$CORRECT_URL"
|
||||
gitlink-cli webhook +test --id $WEBHOOK_ID
|
||||
}
|
||||
|
||||
# 问题3: 事件配置不完整
|
||||
fix_webhook_events() {
|
||||
WEBHOOK_ID=$1
|
||||
DESIRED_EVENTS=$2
|
||||
echo "Updating webhook events for: $WEBHOOK_ID"
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --events "$DESIRED_EVENTS"
|
||||
}
|
||||
|
||||
# 问题4: 密钥过期
|
||||
rotate_webhook_secret() {
|
||||
WEBHOOK_ID=$1
|
||||
NEW_SECRET=$(openssl rand -hex 32)
|
||||
echo "Rotating secret for webhook: $WEBHOOK_ID"
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --secret "$NEW_SECRET"
|
||||
echo "New secret: $NEW_SECRET"
|
||||
echo "Please update the receiving server with the new secret"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 场景6: 安全最佳实践
|
||||
|
||||
### 目标
|
||||
确保 Webhook 配置符合安全最佳实践。
|
||||
|
||||
### 安全配置检查
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# webhook-security-audit.sh
|
||||
|
||||
echo "=== Webhook Security Audit ==="
|
||||
|
||||
# 1. 检查所有 Webhook 是否使用 HTTPS
|
||||
echo "1. Checking HTTPS usage..."
|
||||
insecure_count=0
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
url=$(gitlink-cli webhook +info --id $id --format json | jq -r '.data.hook_url')
|
||||
if [[ ! $url =~ ^https:// ]]; then
|
||||
echo "⚠ Insecure URL found: $url (webhook $id)"
|
||||
insecure_count=$((insecure_count + 1))
|
||||
fi
|
||||
done
|
||||
if [ $insecure_count -eq 0 ]; then
|
||||
echo "✓ All webhooks use HTTPS"
|
||||
else
|
||||
echo "✗ Found $insecure_count webhook(s) using non-HTTPS URLs"
|
||||
fi
|
||||
|
||||
# 2. 检查是否设置了密钥
|
||||
echo "2. Checking secret usage..."
|
||||
no_secret_count=0
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
has_secret=$(gitlink-cli webhook +info --id $id --format json | jq -r '.data.has_secret // false')
|
||||
if [ "$has_secret" = "false" ]; then
|
||||
echo "⚠ Webhook without secret: $id"
|
||||
no_secret_count=$((no_secret_count + 1))
|
||||
fi
|
||||
done
|
||||
if [ $no_secret_count -eq 0 ]; then
|
||||
echo "✓ All webhooks have secrets configured"
|
||||
else
|
||||
echo "⚠ $no_secret_count webhook(s) without secrets"
|
||||
fi
|
||||
|
||||
# 3. 检查 Webhook 数量
|
||||
echo "3. Checking webhook count..."
|
||||
webhook_count=$(gitlink-cli webhook +list --format json | jq -r '.data.total_count')
|
||||
if [ $webhook_count -gt 15 ]; then
|
||||
echo "⚠ High webhook count: $webhook_count (consider cleanup)"
|
||||
else
|
||||
echo "✓ Reasonable webhook count: $webhook_count"
|
||||
fi
|
||||
|
||||
# 4. 检查不活跃的 Webhook
|
||||
echo "4. Checking inactive webhooks..."
|
||||
inactive_count=$(gitlink-cli webhook +list --format json | jq -r '[.data.webhooks[] | select(.is_active == false)] | length')
|
||||
if [ $inactive_count -gt 0 ]; then
|
||||
echo "⚠ Found $inactive_count inactive webhook(s)"
|
||||
echo "Consider removing inactive webhooks:"
|
||||
gitlink-cli webhook +list --format json | jq -r '.data.webhooks[] | select(.is_active == false) | "\(.id): \(.hook_url)"'
|
||||
else
|
||||
echo "✓ All webhooks are active"
|
||||
fi
|
||||
|
||||
echo "=== Security Audit Complete ==="
|
||||
```
|
||||
|
||||
### 安全加固脚本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# webhook-security-hardening.sh
|
||||
|
||||
echo "=== Webhook Security Hardening ==="
|
||||
|
||||
# 1. 为所有 Webhook 添加密钥
|
||||
echo "1. Adding secrets to webhooks without them..."
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
has_secret=$(gitlink-cli webhook +info --id $id --format json | jq -r '.data.has_secret // false')
|
||||
if [ "$has_secret" = "false" ]; then
|
||||
echo "Adding secret to webhook $id..."
|
||||
new_secret=$(openssl rand -hex 32)
|
||||
gitlink-cli webhook +update --id $id --secret "$new_secret"
|
||||
echo "✓ Secret added. Save this secret: $new_secret"
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. 停用不必要的 Webhook
|
||||
echo "2. Reviewing webhooks for necessity..."
|
||||
gitlink-cli webhook +list --format json | jq -r '.data.webhooks[] | "\(.id): \(.description"' | while read -r webhook; do
|
||||
echo "Webhook: $webhook"
|
||||
read -p "Is this webhook still needed? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
webhook_id=$(echo $webhook | cut -d':' -f1)
|
||||
gitlink-cli webhook +delete --id $webhook_id
|
||||
echo "✓ Webhook deleted"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== Security Hardening Complete ==="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
这些工作流示例涵盖了 Webhook 管理的主要场景:
|
||||
|
||||
1. **CI/CD 自动化** - 配置持续集成/部署
|
||||
2. **通知系统** - Issue 和 PR 消息通知
|
||||
3. **多环境部署** - 为不同环境配置独立 Webhook
|
||||
4. **Webhook 迁移** - 安全地迁移 Webhook 配置
|
||||
5. **故障排查** - 诊断和修复 Webhook 问题
|
||||
6. **安全最佳实践** - 确保 Webhook 配置安全
|
||||
|
||||
使用这些示例作为起点,根据您的具体需求进行调整和扩展。
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
# gitlink-cli webhook +create
|
||||
|
||||
创建新的 Webhook,用于自动化通知和集成。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +create \
|
||||
--owner OWNER \
|
||||
--repo REPO \
|
||||
--url URL \
|
||||
[--events EVENTS] \
|
||||
[--active ACTIVE] \
|
||||
[--content_type CONTENT_TYPE] \
|
||||
[--secret SECRET] \
|
||||
[--description DESCRIPTION]
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 短参数 | 说明 | 是否必须 | 默认值 |
|
||||
|------|--------|------|----------|--------|
|
||||
| `--owner` | `-o` | 仓库所有者 | 否 | 自动从 git remote 解析 |
|
||||
| `--repo` | `-r` | 仓库名称 | 否 | 自动从 git remote 解析 |
|
||||
| `--url` | `-u` | Webhook 回调 URL | **是** | - |
|
||||
| `--events` | `-e` | 触发事件(逗号分隔) | 否 | `push` |
|
||||
| `--active` | - | 是否激活 | 否 | `true` |
|
||||
| `--content_type` | - | 内容类型 | 否 | `json` |
|
||||
| `--secret` | - | HMAC 验证密钥 | 否 | 空 |
|
||||
| `--description` | `-d` | Webhook 描述 | 否 | 空 |
|
||||
|
||||
### 事件类型
|
||||
支持的事件类型(多个事件用逗号分隔):
|
||||
- `push` - 代码推送
|
||||
- `pull_request` - Pull 请求
|
||||
- `issue` - Issue 事件
|
||||
- `issue_assign` - Issue 指派
|
||||
- `issue_comment` - Issue 评论
|
||||
- `pull_request_assign` - PR 指派
|
||||
- `pull_request_comment` - PR 评论
|
||||
- `merge_request` - 合并请求
|
||||
- `repository` - 仓库事件
|
||||
- `branch` - 分支事件
|
||||
- `tag` - 标签事件
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功返回
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"id": "456",
|
||||
"hook_url": "https://example.com/webhook",
|
||||
"events": ["push", "pull_request"],
|
||||
"is_active": true,
|
||||
"content_type": "json",
|
||||
"description": "CI/CD webhook",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"project": {
|
||||
"owner": "myuser",
|
||||
"repo": "myrepo"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"identity": "user:myuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误返回
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": 400,
|
||||
"message": "Invalid webhook URL",
|
||||
"suggestion": "Please provide a valid HTTPS URL"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本 Webhook
|
||||
```bash
|
||||
# 创建最简单的 Webhook(仅监听 push 事件)
|
||||
gitlink-cli webhook +create --owner myuser --repo myrepo --url https://example.com/webhook
|
||||
|
||||
# 在 git 仓库目录中创建(自动解析 owner/repo)
|
||||
gitlink-cli webhook +create --url https://example.com/webhook
|
||||
```
|
||||
|
||||
### 多事件 Webhook
|
||||
```bash
|
||||
# 监听多个事件
|
||||
gitlink-cli webhook +create \
|
||||
--owner myuser --repo myrepo \
|
||||
--url https://ci.example.com/webhook \
|
||||
--events push,pull_request,issue
|
||||
|
||||
# 监听所有 PR 相关事件
|
||||
gitlink-cli webhook +create \
|
||||
--url https://review.example.com/webhook \
|
||||
--events pull_request,pull_request_assign,pull_request_comment
|
||||
```
|
||||
|
||||
### 带密钥的 Webhook
|
||||
```bash
|
||||
# 创建带 HMAC 验证密钥的 Webhook
|
||||
gitlink-cli webhook +create \
|
||||
--url https://ci.example.com/webhook \
|
||||
--events push \
|
||||
--secret my-secret-key-12345
|
||||
|
||||
# CI/CD 系统的 Webhook(推荐)
|
||||
gitlink-cli webhook +create \
|
||||
--url https://jenkins.example.com/gitlink-webhook \
|
||||
--events push,pull_request \
|
||||
--secret jenkins-webhook-secret \
|
||||
--description "Jenkins CI trigger"
|
||||
```
|
||||
|
||||
### 带描述的 Webhook
|
||||
```bash
|
||||
# 创建带描述的 Webhook
|
||||
gitlink-cli webhook +create \
|
||||
--url https://notification.example.com/webhook \
|
||||
--events issue,issue_comment \
|
||||
--description "Issue notifications to Slack"
|
||||
```
|
||||
|
||||
### 不激活的 Webhook
|
||||
```bash
|
||||
# 创建 Webhook 但暂时不激活
|
||||
gitlink-cli webhook +create \
|
||||
--url https://example.com/webhook \
|
||||
--events push \
|
||||
--active false \
|
||||
--description "Webhook for testing"
|
||||
```
|
||||
|
||||
### 不同内容类型
|
||||
```bash
|
||||
# JSON 格式(默认)
|
||||
gitlink-cli webhook +create --url https://example.com/webhook --content-type json
|
||||
|
||||
# Form 格式
|
||||
gitlink-cli webhook +create --url https://example.com/webhook --content-type form
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. URL 无效
|
||||
```bash
|
||||
Error: Invalid webhook URL format
|
||||
```
|
||||
**原因**: URL 格式不正确或不是 HTTPS
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 使用 HTTPS URL
|
||||
gitlink-cli webhook +create --url https://example.com/webhook
|
||||
```
|
||||
|
||||
#### 2. 无效的事件类型
|
||||
```bash
|
||||
Error: no valid events specified. Supported events: push, pull_request, issue, ...
|
||||
```
|
||||
**原因**: 指定了不支持的事件类型
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查看支持的事件
|
||||
gitlink-cli webhook +events
|
||||
|
||||
# 使用正确的事件类型
|
||||
gitlink-cli webhook +create --url https://example.com/webhook --events push,pull_request
|
||||
```
|
||||
|
||||
#### 3. 权限不足
|
||||
```bash
|
||||
Error: [403] You don't have permission to create webhooks
|
||||
```
|
||||
**原因**: 用户不是仓库管理员
|
||||
**解决方案**: 确认您有仓库管理员权限
|
||||
|
||||
#### 4. Webhook 数量超限
|
||||
```bash
|
||||
Error: [400] Webhook limit reached (maximum 20 webhooks per repository)
|
||||
```
|
||||
**原因**: 仓库的 Webhook 数量已达上限
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 删除不需要的 Webhook
|
||||
gitlink-cli webhook +delete --id <unused-webhook-id>
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 安全性
|
||||
```bash
|
||||
# 始终为 Webhook 设置密钥
|
||||
gitlink-cli webhook +create \
|
||||
--url https://ci.example.com/webhook \
|
||||
--events push \
|
||||
--secret $(openssl rand -hex 32)
|
||||
|
||||
# 使用 HTTPS URL
|
||||
gitlink-cli webhook +create --url https://example.com/webhook
|
||||
```
|
||||
|
||||
### 2. 事件选择
|
||||
```bash
|
||||
# 只监听必要的事件
|
||||
gitlink-cli webhook +create \
|
||||
--url https://ci.example.com/webhook \
|
||||
--events push # CI 只需要 push 事件
|
||||
```
|
||||
|
||||
### 3. 描述清晰
|
||||
```bash
|
||||
# 添加清晰的描述便于管理
|
||||
gitlink-cli webhook +create \
|
||||
--url https://jenkins.example.com/webhook \
|
||||
--events push,pull_request \
|
||||
--description "Production CI - Jenkins Pipeline"
|
||||
```
|
||||
|
||||
## AI Agent 使用建议
|
||||
|
||||
### 验证 Webhook 创建
|
||||
```bash
|
||||
# 创建后立即测试
|
||||
WEBHOOK_ID=$(gitlink-cli webhook +create --url $URL --events $EVENTS --format json | jq -r '.data.id')
|
||||
gitlink-cli webhook +test --id $WEBHOOK_ID
|
||||
|
||||
# 验证 Webhook 配置
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID
|
||||
```
|
||||
|
||||
### 检查重复 Webhook
|
||||
```bash
|
||||
# 检查是否已存在相同 URL 的 Webhook
|
||||
existing=$(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[] | select(.hook_url == "https://example.com/webhook") | .id')
|
||||
if [ -n "$existing" ]; then
|
||||
echo "Webhook already exists: $existing"
|
||||
else
|
||||
gitlink-cli webhook +create --url https://example.com/webhook
|
||||
fi
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **使用密钥**: 始终设置 `--secret` 参数以验证请求来源
|
||||
2. **HTTPS**: 确保使用 HTTPS URL 保护数据传输
|
||||
3. **最小权限**: 只监听必要的事件类型
|
||||
4. **定期轮换**: 定期更新 Webhook 密钥
|
||||
5. **监控日志**: 监控 Webhook 请求日志以发现异常活动
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **URL 要求**: Webhook URL 必须是公网可访问的 HTTPS 地址
|
||||
2. **数量限制**: 每个仓库最多 20 个 Webhook
|
||||
3. **权限要求**: 需要仓库管理员权限
|
||||
4. **事件格式**: 多个事件用逗号分隔,不要有空格
|
||||
5. **立即生效**: 创建后立即可用,除非设置 `--active false`
|
||||
|
||||
## 相关命令
|
||||
|
||||
- `webhook +list` - 列出所有 Webhook
|
||||
- `webhook +update` - 更新 Webhook 配置
|
||||
- `webhook +test` - 测试 Webhook
|
||||
- `webhook +events` - 查看支持的事件类型
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
# gitlink-cli webhook +delete
|
||||
|
||||
删除指定的 Webhook。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +delete [--owner OWNER] [--repo REPO] --id WEBHOOK_ID
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 短参数 | 说明 | 是否必须 | 默认值 |
|
||||
|------|--------|------|----------|--------|
|
||||
| `--owner` | `-o` | 仓库所有者 | 否 | 自动从 git remote 解析 |
|
||||
| `--repo` | `-r` | 仓库名称 | 否 | 自动从 git remote 解析 |
|
||||
| `--id` | `-i` | Webhook ID | **是** | - |
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功返回
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"message": "Webhook deleted successfully"
|
||||
},
|
||||
"meta": {
|
||||
"identity": "user:myuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误返回
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": 404,
|
||||
"message": "Webhook not found",
|
||||
"suggestion": "Please check the webhook ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本用法
|
||||
```bash
|
||||
# 删除指定 Webhook
|
||||
gitlink-cli webhook +delete --owner myuser --repo myrepo --id 456
|
||||
|
||||
# 在 git 仓库目录中删除(自动解析 owner/repo)
|
||||
gitlink-cli webhook +delete --id 456
|
||||
|
||||
# 使用短参数
|
||||
gitlink-cli webhook +delete -i 456
|
||||
```
|
||||
|
||||
### 删除多个 Webhook
|
||||
```bash
|
||||
# 批量删除多个 Webhook
|
||||
for id in 123 456 789; do
|
||||
gitlink-cli webhook +delete --id $id
|
||||
done
|
||||
```
|
||||
|
||||
### 交互式删除
|
||||
```bash
|
||||
# 先查看 Webhook 详情确认
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 确认后删除
|
||||
gitlink-cli webhook +delete --id 456
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. Webhook 不存在
|
||||
```bash
|
||||
Error: [404] Webhook not found
|
||||
```
|
||||
**原因**: 指定的 Webhook ID 不存在或已被删除
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 先列出所有 Webhook 确认 ID
|
||||
gitlink-cli webhook +list
|
||||
```
|
||||
|
||||
#### 2. 权限不足
|
||||
```bash
|
||||
Error: [403] You don't have permission to delete webhooks
|
||||
```
|
||||
**原因**: 用户不是仓库管理员
|
||||
**解决方案**: 确认您有仓库管理员权限
|
||||
|
||||
#### 3. ID 参数缺失
|
||||
```bash
|
||||
Error: required flag --id is missing
|
||||
```
|
||||
**原因**: 没有提供 Webhook ID
|
||||
**解决方案**: 指定要删除的 Webhook ID
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 删除前确认
|
||||
```bash
|
||||
# 删除前先查看 Webhook 详情
|
||||
WEBHOOK_ID=456
|
||||
echo "About to delete webhook:"
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID
|
||||
|
||||
# 确认后删除
|
||||
read -p "Confirm deletion? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
gitlink-cli webhook +delete --id $WEBHOOK_ID
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. 记录删除的 Webhook
|
||||
```bash
|
||||
# 删除前记录 Webhook 配置
|
||||
WEBHOOK_ID=456
|
||||
BACKUP_FILE="webhook_backup_$WEBHOOK_ID.json"
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID --format json > $BACKUP_FILE
|
||||
echo "Webhook config backed up to $BACKUP_FILE"
|
||||
|
||||
# 然后删除
|
||||
gitlink-cli webhook +delete --id $WEBHOOK_ID
|
||||
```
|
||||
|
||||
### 3. 批量清理不活跃的 Webhook
|
||||
```bash
|
||||
# 列出所有不活跃的 Webhook 并删除
|
||||
inactive_webhooks=$(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[] | select(.is_active == false) | .id')
|
||||
for id in $inactive_webhooks; do
|
||||
echo "Deleting inactive webhook: $id"
|
||||
gitlink-cli webhook +delete --id $id
|
||||
done
|
||||
```
|
||||
|
||||
## AI Agent 使用建议
|
||||
|
||||
### 安全删除流程
|
||||
```bash
|
||||
# AI Agent 删除 Webhook 的安全流程
|
||||
delete_webhook_safely() {
|
||||
WEBHOOK_ID=$1
|
||||
|
||||
# 1. 检查 Webhook 是否存在
|
||||
if ! gitlink-cli webhook +info --id $WEBHOOK_ID --format json >/dev/null 2>&1; then
|
||||
echo "Webhook $WEBHOOK_ID not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 2. 备份配置
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID --format json > "webhook_backup_$WEBHOOK_ID.json"
|
||||
|
||||
# 3. 删除 Webhook
|
||||
if gitlink-cli webhook +delete --id $WEBHOOK_ID; then
|
||||
echo "Webhook $WEBHOOK_ID deleted successfully"
|
||||
return 0
|
||||
else
|
||||
echo "Failed to delete webhook $WEBHOOK_ID"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### 批量删除 Webhook
|
||||
```bash
|
||||
# 删除所有匹配特定条件的 Webhook
|
||||
delete_webhooks_by_url() {
|
||||
URL_PATTERN=$1
|
||||
|
||||
# 找到匹配的 Webhook
|
||||
webhook_ids=$(gitlink-cli webhook +list --format json | \
|
||||
jq -r ".data.webhooks[] | select(.hook_url | contains(\"$URL_PATTERN\")) | .id")
|
||||
|
||||
# 逐个删除
|
||||
for id in $webhook_ids; do
|
||||
echo "Deleting webhook $id with URL matching $URL_PATTERN"
|
||||
gitlink-cli webhook +delete --id $id
|
||||
done
|
||||
}
|
||||
|
||||
# 使用示例:删除所有指向旧服务器的 Webhook
|
||||
delete_webhooks_by_url "old-server.example.com"
|
||||
```
|
||||
|
||||
### 验证删除
|
||||
```bash
|
||||
# 删除 Webhook 并验证
|
||||
WEBHOOK_ID=456
|
||||
|
||||
# 删除前检查
|
||||
if gitlink-cli webhook +info --id $WEBHOOK_ID >/dev/null 2>&1; then
|
||||
echo "Webhook exists, deleting..."
|
||||
gitlink-cli webhook +delete --id $WEBHOOK_ID
|
||||
|
||||
# 验证删除成功
|
||||
if ! gitlink-cli webhook +info --id $WEBHOOK_ID >/dev/null 2>&1; then
|
||||
echo "Webhook deleted successfully"
|
||||
else
|
||||
echo "Webhook still exists after deletion"
|
||||
fi
|
||||
else
|
||||
echo "Webhook not found"
|
||||
fi
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不可恢复**: 删除操作不可逆,请谨慎操作
|
||||
2. **立即生效**: 删除后立即停止接收事件
|
||||
3. **权限要求**: 需要仓库管理员权限
|
||||
4. **API 特性**: GitLink API 在删除时可能返回错误信息,但实际删除成功
|
||||
5. **验证删除**: 建议删除后验证 Webhook 是否已删除
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 场景1: 清理测试 Webhook
|
||||
```bash
|
||||
# 删除所有测试环境的 Webhook
|
||||
test_webhooks=$(gitlink-cli webhook +list --format json | \
|
||||
jq -r '.data.webhooks[] | select(.description | contains("test")) | .id')
|
||||
|
||||
for id in $test_webhooks; do
|
||||
echo "Deleting test webhook: $id"
|
||||
gitlink-cli webhook +delete --id $id
|
||||
done
|
||||
```
|
||||
|
||||
### 场景2: 迁移到新 URL
|
||||
```bash
|
||||
# 迁移 Webhook 到新 URL
|
||||
OLD_WEBHOOK_ID=456
|
||||
OLD_URL=$(gitlink-cli webhook +info --id $OLD_WEBHOOK_ID --format json | jq -r '.data.hook_url')
|
||||
NEW_URL="https://new-server.example.com/webhook"
|
||||
|
||||
# 创建新 Webhook
|
||||
NEW_WEBHOOK_ID=$(gitlink-cli webhook +create --url $NEW_URL --events push --format json | jq -r '.data.id')
|
||||
|
||||
# 测试新 Webhook
|
||||
gitlink-cli webhook +test --id $NEW_WEBHOOK_ID
|
||||
|
||||
# 确认新 Webhook 工作后删除旧 Webhook
|
||||
gitlink-cli webhook +delete --id $OLD_WEBHOOK_ID
|
||||
```
|
||||
|
||||
### 场景3: 批量重构 Webhook
|
||||
```bash
|
||||
# 重构所有 Webhook,重新创建后删除旧的
|
||||
# 1. 备份现有配置
|
||||
gitlink-cli webhook +list --format json > webhook_config_backup.json
|
||||
|
||||
# 2. 根据备份创建新配置(可能使用不同的 URL 或事件)
|
||||
|
||||
# 3. 删除旧的 Webhook
|
||||
old_ids=$(jq -r '.data.webhooks[].id' webhook_config_backup.json)
|
||||
for id in $old_ids; do
|
||||
gitlink-cli webhook +delete --id $id
|
||||
done
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **删除前备份**: 删除前备份 Webhook 配置
|
||||
2. **确认操作**: 删除前确认 Webhook ID 和配置
|
||||
3. **逐步删除**: 批量删除时逐步进行,避免误删
|
||||
4. **验证删除**: 删除后验证 Webhook 已被删除
|
||||
5. **权限控制**: 限制删除权限给授权用户
|
||||
|
||||
## 相关命令
|
||||
|
||||
- `webhook +list` - 列出所有 Webhook
|
||||
- `webhook +info` - 查看 Webhook 详情
|
||||
- `webhook +create` - 创建新 Webhook
|
||||
- `webhook +update` - 更新 Webhook(可以先用 `--active false` 停用)
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
# gitlink-cli webhook +info
|
||||
|
||||
查看指定 Webhook 的详细信息。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +info [--owner OWNER] [--repo REPO] --id WEBHOOK_ID
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 短参数 | 说明 | 是否必须 | 默认值 |
|
||||
|------|--------|------|----------|--------|
|
||||
| `--owner` | `-o` | 仓库所有者 | 否 | 自动从 git remote 解析 |
|
||||
| `--repo` | `-r` | 仓库名称 | 否 | 自动从 git remote 解析 |
|
||||
| `--id` | `-i` | Webhook ID | **是** | - |
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功返回
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"id": "456",
|
||||
"hook_url": "https://ci.example.com/webhook",
|
||||
"events": ["push", "pull_request", "issue"],
|
||||
"is_active": true,
|
||||
"content_type": "json",
|
||||
"description": "CI/CD automation webhook",
|
||||
"project": {
|
||||
"owner": "myuser",
|
||||
"repo": "myrepo",
|
||||
"identifier": "myuser/myrepo"
|
||||
},
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-15T12:30:00Z",
|
||||
"last_delivery": {
|
||||
"timestamp": "2024-01-15T14:25:00Z",
|
||||
"status": "success",
|
||||
"event": "push",
|
||||
"duration_ms": 245
|
||||
},
|
||||
"delivery_statistics": {
|
||||
"total_deliveries": 1523,
|
||||
"successful_deliveries": 1498,
|
||||
"failed_deliveries": 25,
|
||||
"success_rate": 98.36
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"identity": "user:myuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误返回
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": 404,
|
||||
"message": "Webhook not found",
|
||||
"suggestion": "Please check the webhook ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本用法
|
||||
```bash
|
||||
# 查看 Webhook 详情
|
||||
gitlink-cli webhook +info --owner myuser --repo myrepo --id 456
|
||||
|
||||
# 在 git 仓库目录中查看(自动解析 owner/repo)
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 使用短参数
|
||||
gitlink-cli webhook +info -i 456
|
||||
```
|
||||
|
||||
### 不同输出格式
|
||||
```bash
|
||||
# JSON 格式(默认,便于解析)
|
||||
gitlink-cli webhook +info --id 456 --format json
|
||||
|
||||
# Table 格式(更易阅读)
|
||||
gitlink-cli webhook +info --id 456 --format table
|
||||
|
||||
# YAML 格式
|
||||
gitlink-cli webhook +info --id 456 --format yaml
|
||||
```
|
||||
|
||||
### 提取特定信息
|
||||
```bash
|
||||
# 使用 jq 提取 Webhook URL
|
||||
gitlink-cli webhook +info --id 456 --format json | jq -r '.data.hook_url'
|
||||
|
||||
# 查看 Webhook 是否激活
|
||||
gitlink-cli webhook +info --id 456 --format json | jq -r '.data.is_active'
|
||||
|
||||
# 查看监听的事件类型
|
||||
gitlink-cli webhook +info --id 456 --format json | jq -r '.data.events[]'
|
||||
|
||||
# 查看统计信息
|
||||
gitlink-cli webhook +info --id 456 --format json | jq '.data.delivery_statistics'
|
||||
```
|
||||
|
||||
### 比较两个 Webhook
|
||||
```bash
|
||||
# 比较两个 Webhook 的配置
|
||||
echo "=== Webhook 456 ==="
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
echo "=== Webhook 789 ==="
|
||||
gitlink-cli webhook +info --id 789
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. Webhook 不存在
|
||||
```bash
|
||||
Error: [404] Webhook not found
|
||||
```
|
||||
**原因**: 指定的 Webhook ID 不存在
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 先列出所有 Webhook 找到正确 ID
|
||||
gitlink-cli webhook +list
|
||||
```
|
||||
|
||||
#### 2. 权限不足
|
||||
```bash
|
||||
Error: [403] You don't have permission to view webhook details
|
||||
```
|
||||
**原因**: 用户没有仓库访问权限
|
||||
**解决方案**: 确认您是仓库成员
|
||||
|
||||
#### 3. ID 参数缺失
|
||||
```bash
|
||||
Error: required flag --id is missing
|
||||
```
|
||||
**原因**: 没有提供 Webhook ID
|
||||
**解决方案**: 指定要查看的 Webhook ID
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 更新前查看
|
||||
```bash
|
||||
# 更新 Webhook 前先查看当前配置
|
||||
WEBHOOK_ID=456
|
||||
echo "Current configuration:"
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID
|
||||
|
||||
# 然后进行更新
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --url $NEW_URL
|
||||
```
|
||||
|
||||
### 2. 批量查看 Webhook 信息
|
||||
```bash
|
||||
# 查看所有 Webhook 的简要信息
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
echo "=== Webhook $id ==="
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID --format json | jq -r '.data | "\(.hook_url) - \(.description)"'
|
||||
done
|
||||
```
|
||||
|
||||
### 3. 验证 Webhook 配置
|
||||
```bash
|
||||
# 检查 Webhook 是否正确配置
|
||||
check_webhook_config() {
|
||||
WEBHOOK_ID=$1
|
||||
|
||||
# 获取 Webhook 信息
|
||||
info=$(gitlink-cli webhook +info --id $WEBHOOK_ID --format json)
|
||||
|
||||
# 检查是否激活
|
||||
is_active=$(echo $info | jq -r '.data.is_active')
|
||||
if [ "$is_active" != "true" ]; then
|
||||
echo "WARNING: Webhook is not active"
|
||||
fi
|
||||
|
||||
# 检查是否有事件
|
||||
events=$(echo $info | jq -r '.data.events | length')
|
||||
if [ "$events" -eq 0 ]; then
|
||||
echo "WARNING: No events configured"
|
||||
fi
|
||||
|
||||
# 检查 URL 是否有效
|
||||
url=$(echo $info | jq -r '.data.hook_url')
|
||||
if [[ ! $url =~ ^https:// ]]; then
|
||||
echo "WARNING: URL does not use HTTPS"
|
||||
fi
|
||||
|
||||
# 显示成功率
|
||||
success_rate=$(echo $info | jq -r '.data.delivery_statistics.success_rate')
|
||||
echo "Success rate: $success_rate%"
|
||||
}
|
||||
```
|
||||
|
||||
## AI Agent 使用建议
|
||||
|
||||
### 自动化 Webhook 配置检查
|
||||
```bash
|
||||
# AI Agent 检查 Webhook 配置的自动化脚本
|
||||
analyze_webhook() {
|
||||
WEBHOOK_ID=$1
|
||||
OUTPUT_FORMAT="${2:-json}"
|
||||
|
||||
info=$(gitlink-cli webhook +info --id $WEBHOOK_ID --format $OUTPUT_FORMAT)
|
||||
|
||||
if [ "$OUTPUT_FORMAT" = "json" ]; then
|
||||
# JSON 格式便于解析
|
||||
echo "$info" | jq '.data | {
|
||||
id,
|
||||
url: .hook_url,
|
||||
active: .is_active,
|
||||
events: .events,
|
||||
success_rate: .delivery_statistics.success_rate,
|
||||
last_delivery: .last_delivery.timestamp
|
||||
}'
|
||||
else
|
||||
# 其他格式直接输出
|
||||
echo "$info"
|
||||
fi
|
||||
}
|
||||
|
||||
# 批量分析所有 Webhook
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
analyze_webhook $id
|
||||
done
|
||||
```
|
||||
|
||||
### Webhook 健康检查
|
||||
```bash
|
||||
# 检查 Webhook 健康状态
|
||||
check_webhook_health() {
|
||||
WEBHOOK_ID=$1
|
||||
|
||||
info=$(gitlink-cli webhook +info --id $WEBHOOK_ID --format json)
|
||||
|
||||
# 提取关键指标
|
||||
is_active=$(echo $info | jq -r '.data.is_active')
|
||||
success_rate=$(echo $info | jq -r '.data.delivery_statistics.success_rate')
|
||||
last_delivery=$(echo $info | jq -r '.data.last_delivery.timestamp')
|
||||
|
||||
# 健康评分
|
||||
health_score=100
|
||||
issues=()
|
||||
|
||||
if [ "$is_active" != "true" ]; then
|
||||
health_score=$((health_score - 50))
|
||||
issues+=("Webhook is not active")
|
||||
fi
|
||||
|
||||
if (( $(echo "$success_rate < 95" | bc -l) )); then
|
||||
health_score=$((health_score - 30))
|
||||
issues+=("Success rate below 95%: $success_rate%")
|
||||
fi
|
||||
|
||||
if [ -z "$last_delivery" ] || [ "$last_delivery" = "null" ]; then
|
||||
health_score=$((health_score - 20))
|
||||
issues+=("No recent deliveries")
|
||||
fi
|
||||
|
||||
# 输出结果
|
||||
echo "Webhook $WEBHOOK_ID Health Check"
|
||||
echo "Health Score: $health_score/100"
|
||||
if [ ${#issues[@]} -gt 0 ]; then
|
||||
echo "Issues found:"
|
||||
printf '%s\n' "${issues[@]}"
|
||||
else
|
||||
echo "✓ Webhook is healthy"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### 配置差异分析
|
||||
```bash
|
||||
# 比较两个 Webhook 的配置差异
|
||||
compare_webhooks() {
|
||||
ID1=$1
|
||||
ID2=$2
|
||||
|
||||
info1=$(gitlink-cli webhook +info --id $ID1 --format json)
|
||||
info2=$(gitlink-cli webhook +info --id $ID2 --format json)
|
||||
|
||||
echo "=== Webhook Comparison ==="
|
||||
echo "Webhook 1: $ID1"
|
||||
echo "Webhook 2: $ID2"
|
||||
echo
|
||||
|
||||
# 比较 URL
|
||||
url1=$(echo $info1 | jq -r '.data.hook_url')
|
||||
url2=$(echo $info2 | jq -r '.data.hook_url')
|
||||
echo "URL:"
|
||||
echo " $ID1: $url1"
|
||||
echo " $ID2: $url2"
|
||||
[ "$url1" = "$url2" ] && echo " Status: Same" || echo " Status: Different"
|
||||
echo
|
||||
|
||||
# 比较事件
|
||||
events1=$(echo $info1 | jq -r '.data.events | sort | join(",")')
|
||||
events2=$(echo $info2 | jq -r '.data.events | sort | join(",")')
|
||||
echo "Events:"
|
||||
echo " $ID1: $events1"
|
||||
echo " $ID2: $events2"
|
||||
[ "$events1" = "$events2" ] && echo " Status: Same" || echo " Status: Different"
|
||||
echo
|
||||
|
||||
# 比较激活状态
|
||||
active1=$(echo $info1 | jq -r '.data.is_active')
|
||||
active2=$(echo $info2 | jq -r '.data.is_active')
|
||||
echo "Active Status:"
|
||||
echo " $ID1: $active1"
|
||||
echo " $ID2: $active2"
|
||||
[ "$active1" = "$active2" ] && echo " Status: Same" || echo " Status: Different"
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限要求**: 至少需要仓库读取权限
|
||||
2. **详细信息**: 包含 Webhook 的所有配置和统计信息
|
||||
3. **统计数据**: 部分统计信息可能为空,特别是新创建的 Webhook
|
||||
4. **时间格式**: 所有时间戳均为 ISO 8601 格式(UTC)
|
||||
5. **敏感信息**: 输出可能包含敏感信息,注意保护
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 场景1: 确认 Webhook 配置
|
||||
```bash
|
||||
# 确认 Webhook 配置是否正确
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 检查关键配置
|
||||
gitlink-cli webhook +info --id 456 --format json | jq -r '{
|
||||
url: .data.hook_url,
|
||||
events: .data.events,
|
||||
active: .data.is_active,
|
||||
success_rate: .data.delivery_statistics.success_rate
|
||||
}'
|
||||
```
|
||||
|
||||
### 场景2: 故障排查
|
||||
```bash
|
||||
# Webhook 出问题时查看详细信息
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 检查最近一次投递情况
|
||||
gitlink-cli webhook +info --id 456 --format json | jq '.data.last_delivery'
|
||||
|
||||
# 查看失败统计
|
||||
gitlink-cli webhook +info --id 456 --format json | jq '.data.delivery_statistics'
|
||||
```
|
||||
|
||||
### 场景3: 配置审计
|
||||
```bash
|
||||
# 审计所有 Webhook 配置
|
||||
echo "=== Webhook Configuration Audit ==="
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
echo "Webhook $id:"
|
||||
gitlink-cli webhook +info --id $id --format json | jq -r '{
|
||||
url: .data.hook_url,
|
||||
events: .data.events | join(","),
|
||||
active: .data.is_active,
|
||||
description: .description
|
||||
}'
|
||||
echo
|
||||
done
|
||||
```
|
||||
|
||||
## 相关命令
|
||||
|
||||
- `webhook +list` - 列出所有 Webhook
|
||||
- `webhook +create` - 创建新 Webhook
|
||||
- `webhook +update` - 更新 Webhook 配置
|
||||
- `webhook +test` - 测试 Webhook
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
# gitlink-cli webhook +list
|
||||
|
||||
列出仓库的所有 Webhook。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +list [--owner OWNER] [--repo REPO] [--page PAGE] [--limit LIMIT]
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 短参数 | 说明 | 是否必须 | 默认值 |
|
||||
|------|--------|------|----------|--------|
|
||||
| `--owner` | `-o` | 仓库所有者 | 否 | 自动从 git remote 解析 |
|
||||
| `--repo` | `-r` | 仓库名称 | 否 | 自动从 git remote 解析 |
|
||||
| `--page` | `-p` | 页码 | 否 | 1 |
|
||||
| `--limit` | `-l` | 每页数量 | 否 | 20 |
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功返回
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"webhooks": [
|
||||
{
|
||||
"id": "123",
|
||||
"hook_url": "https://example.com/webhook",
|
||||
"events": ["push", "pull_request"],
|
||||
"is_active": true,
|
||||
"content_type": "json",
|
||||
"description": "CI/CD webhook",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 5,
|
||||
"page": 1,
|
||||
"limit": 20
|
||||
},
|
||||
"meta": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total_count": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误返回
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": 401,
|
||||
"message": "Authentication failed",
|
||||
"suggestion": "Please run 'gitlink-cli auth login' to authenticate"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本用法
|
||||
```bash
|
||||
# 列出当前仓库的 Webhook(需要在 git 仓库目录中)
|
||||
gitlink-cli webhook +list
|
||||
|
||||
# 列出指定仓库的 Webhook
|
||||
gitlink-cli webhook +list --owner myuser --repo myrepo
|
||||
|
||||
# 分页显示
|
||||
gitlink-cli webhook +list --owner myuser --repo myrepo --page 2 --limit 10
|
||||
```
|
||||
|
||||
### JSON 格式输出(AI Agent 使用)
|
||||
```bash
|
||||
# 获取 JSON 格式输出便于解析
|
||||
gitlink-cli webhook +list --owner myuser --repo myrepo --format json
|
||||
|
||||
# 使用 jq 处理输出
|
||||
gitlink-cli webhook +list --format json | jq '.data.webhooks[] | select(.is_active == true)'
|
||||
|
||||
# 统计 Webhook 数量
|
||||
gitlink-cli webhook +list --format json | jq '.data.total_count'
|
||||
```
|
||||
|
||||
### Table 格式输出
|
||||
```bash
|
||||
# 表格格式更易阅读(默认)
|
||||
gitlink-cli webhook +list --format table
|
||||
|
||||
# 指定表格格式
|
||||
gitlink-cli webhook +list --owner myuser --repo myrepo --format table
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. 认证失败
|
||||
```bash
|
||||
Error: [401] Authentication failed
|
||||
```
|
||||
**原因**: Token 过期或无效
|
||||
**解决方案**:
|
||||
```bash
|
||||
gitlink-cli auth login
|
||||
```
|
||||
|
||||
#### 2. 权限不足
|
||||
```bash
|
||||
Error: [403] You don't have permission to view webhooks
|
||||
```
|
||||
**原因**: 用户没有仓库访问权限
|
||||
**解决方案**: 确认您是仓库成员或公开项目
|
||||
|
||||
#### 3. 仓库不存在
|
||||
```bash
|
||||
Error: [404] Repository not found
|
||||
```
|
||||
**原因**: 仓库名称或所有者错误
|
||||
**解决方案**: 使用 `gitlink-cli repo +list` 确认仓库名称
|
||||
|
||||
## AI Agent 使用建议
|
||||
|
||||
### 检查 Webhook 配置
|
||||
```bash
|
||||
# 检查是否已配置特定类型的 Webhook
|
||||
gitlink-cli webhook +list --format json | jq '.data.webhooks[] | select(.hook_url | contains("ci-system"))'
|
||||
|
||||
# 检查是否有激活的 Webhook
|
||||
gitlink-cli webhook +list --format json | jq '.data.webhooks[] | select(.is_active == true)'
|
||||
|
||||
# 获取所有 Webhook 的 URL
|
||||
gitlink-cli webhook +list --format json | jq '.data.webhooks[].hook_url'
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
```bash
|
||||
# 获取所有 Webhook ID
|
||||
webhook_ids=$(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id')
|
||||
|
||||
# 批量测试所有 Webhook
|
||||
for id in $webhook_ids; do
|
||||
gitlink-cli webhook +test --id $id
|
||||
done
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **分页查询**: 默认每页显示 20 个,使用 `--limit` 可调整
|
||||
2. **权限要求**: 至少需要仓库读取权限
|
||||
3. **自动解析**: 在 git 仓库目录中可省略 `--owner` 和 `--repo`
|
||||
4. **格式选择**: AI Agent 建议使用 `--format json` 便于解析
|
||||
|
||||
## 相关命令
|
||||
|
||||
- `webhook +create` - 创建新 Webhook
|
||||
- `webhook +info` - 查看特定 Webhook 详情
|
||||
- `webhook +events` - 查看支持的事件类型
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
# gitlink-cli webhook +test
|
||||
|
||||
测试 Webhook 连接性,发送测试事件验证 Webhook 是否正常工作。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +test \
|
||||
[--owner OWNER] \
|
||||
[--repo REPO] \
|
||||
--id WEBHOOK_ID \
|
||||
[--event EVENT_TYPE]
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 短参数 | 说明 | 是否必须 | 默认值 |
|
||||
|------|--------|------|----------|--------|
|
||||
| `--owner` | `-o` | 仓库所有者 | 否 | 自动从 git remote 解析 |
|
||||
| `--repo` | `-r` | 仓库名称 | 否 | 自动从 git remote 解析 |
|
||||
| `--id` | `-i` | Webhook ID | **是** | - |
|
||||
| `--event` | `-e` | 测试的事件类型 | 否 | `push` |
|
||||
|
||||
### 支持的测试事件
|
||||
- `push` - 推送事件(默认)
|
||||
- `pull_request` - Pull 请求事件
|
||||
- `issue` - Issue 事件
|
||||
- 其他支持的事件类型
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功返回
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"message": "Webhook test triggered successfully",
|
||||
"webhook_id": "456",
|
||||
"event_type": "push",
|
||||
"delivered": true,
|
||||
"response_status": 200,
|
||||
"response_body": "Webhook received"
|
||||
},
|
||||
"meta": {
|
||||
"identity": "user:myuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook 不可达
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"message": "Webhook test completed with warnings",
|
||||
"webhook_id": "456",
|
||||
"event_type": "push",
|
||||
"delivered": false,
|
||||
"error": "Connection timeout",
|
||||
"suggestion": "Please check if the webhook URL is accessible"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误返回
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": 404,
|
||||
"message": "Webhook not found",
|
||||
"suggestion": "Please check the webhook ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本测试
|
||||
```bash
|
||||
# 测试 Webhook(默认使用 push 事件)
|
||||
gitlink-cli webhook +test --owner myuser --repo myrepo --id 456
|
||||
|
||||
# 在 git 仓库目录中测试
|
||||
gitlink-cli webhook +test --id 456
|
||||
|
||||
# 使用短参数
|
||||
gitlink-cli webhook +test -i 456
|
||||
```
|
||||
|
||||
### 测试特定事件类型
|
||||
```bash
|
||||
# 测试 pull_request 事件
|
||||
gitlink-cli webhook +test --id 456 --event pull_request
|
||||
|
||||
# 测试 issue 事件
|
||||
gitlink-cli webhook +test --id 456 --event issue
|
||||
|
||||
# 测试多种事件类型
|
||||
for event in push pull_request issue; do
|
||||
echo "Testing event: $event"
|
||||
gitlink-cli webhook +test --id 456 --event $event
|
||||
done
|
||||
```
|
||||
|
||||
### 批量测试所有 Webhook
|
||||
```bash
|
||||
# 测试仓库的所有 Webhook
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
echo "Testing webhook: $id"
|
||||
gitlink-cli webhook +test --id $id
|
||||
done
|
||||
```
|
||||
|
||||
### 测试新创建的 Webhook
|
||||
```bash
|
||||
# 创建后立即测试
|
||||
WEBHOOK_ID=$(gitlink-cli webhook +create --url https://example.com/webhook --events push --format json | jq -r '.data.id')
|
||||
echo "Testing new webhook: $WEBHOOK_ID"
|
||||
gitlink-cli webhook +test --id $WEBHOOK_ID
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. Webhook 不存在
|
||||
```bash
|
||||
Error: [404] Webhook not found
|
||||
```
|
||||
**原因**: 指定的 Webhook ID 不存在
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 先列出所有 Webhook 找到正确 ID
|
||||
gitlink-cli webhook +list
|
||||
```
|
||||
|
||||
#### 2. 无效的事件类型
|
||||
```bash
|
||||
Error: unsupported event type: custom_event
|
||||
```
|
||||
**原因**: 指定了不支持的事件类型
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查看支持的事件类型
|
||||
gitlink-cli webhook +events
|
||||
|
||||
# 使用支持的事件类型
|
||||
gitlink-cli webhook +test --id 456 --event push
|
||||
```
|
||||
|
||||
#### 3. Webhook URL 不可达
|
||||
```bash
|
||||
Warning: Webhook delivery failed - Connection timeout
|
||||
```
|
||||
**原因**: Webhook URL 无法访问或服务器无响应
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 1. 检查 URL 是否正确
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 2. 手动测试 URL
|
||||
curl -X POST https://your-webhook-url.com/test
|
||||
|
||||
# 3. 检查服务器防火墙和网络设置
|
||||
```
|
||||
|
||||
#### 4. SSL 证书问题
|
||||
```bash
|
||||
Warning: Webhook delivery failed - SSL certificate verify failed
|
||||
```
|
||||
**原因**: Webhook 服务器的 SSL 证书有问题
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 检查 SSL 证书
|
||||
curl -v https://your-webhook-url.com/test
|
||||
|
||||
# 更新服务器的 SSL 证书
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 创建后测试
|
||||
```bash
|
||||
# 创建 Webhook 后立即测试
|
||||
WEBHOOK_ID=$(gitlink-cli webhook +create --url $URL --events $EVENTS --format json | jq -r '.data.id')
|
||||
if gitlink-cli webhook +test --id $WEBHOOK_ID; then
|
||||
echo "Webhook created and tested successfully"
|
||||
else
|
||||
echo "Webhook test failed, please check configuration"
|
||||
gitlink-cli webhook +delete --id $WEBHOOK_ID
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. 更新后测试
|
||||
```bash
|
||||
# 更新 Webhook 后测试
|
||||
gitlink-cli webhook +update --id 456 --url $NEW_URL
|
||||
gitlink-cli webhook +test --id 456
|
||||
```
|
||||
|
||||
### 3. 定期测试
|
||||
```bash
|
||||
# 定期测试所有 Webhook 确保正常工作
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
if ! gitlink-cli webhook +test --id $id; then
|
||||
echo "WARNING: Webhook $id test failed"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## AI Agent 使用建议
|
||||
|
||||
### 自动化测试流程
|
||||
```bash
|
||||
# AI Agent 测试 Webhook 的完整流程
|
||||
test_and_fix_webhook() {
|
||||
WEBHOOK_ID=$1
|
||||
MAX_RETRIES=3
|
||||
RETRY_COUNT=0
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||
echo "Testing webhook $WEBHOOK_ID (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)"
|
||||
|
||||
# 测试 Webhook
|
||||
if gitlink-cli webhook +test --id $WEBHOOK_ID; then
|
||||
echo "✓ Webhook test successful"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 测试失败,等待后重试
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
|
||||
echo "Test failed, waiting 5 seconds before retry..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✗ Webhook test failed after $MAX_RETRIES attempts"
|
||||
return 1
|
||||
}
|
||||
```
|
||||
|
||||
### 监控 Webhook 健康
|
||||
```bash
|
||||
# 定期检查所有 Webhook 的健康状态
|
||||
check_all_webhooks_health() {
|
||||
REPORT_FILE="webhook_health_report_$(date +%Y%m%d_%H%M%S).txt"
|
||||
|
||||
echo "Webhook Health Check Report - $(date)" > $REPORT_FILE
|
||||
echo "=================================" >> $REPORT_FILE
|
||||
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
webhook_info=$(gitlink-cli webhook +info --id $id --format json)
|
||||
webhook_url=$(echo $webhook_info | jq -r '.data.hook_url')
|
||||
webhook_status=$(echo $webhook_info | jq -r '.data.is_active')
|
||||
|
||||
echo -e "\nWebhook ID: $id" >> $REPORT_FILE
|
||||
echo "URL: $webhook_url" >> $REPORT_FILE
|
||||
echo "Active: $webhook_status" >> $REPORT_FILE
|
||||
echo "Test Result:" >> $REPORT_FILE
|
||||
|
||||
if gitlink-cli webhook +test --id $id >> $REPORT_FILE 2>&1; then
|
||||
echo "Status: HEALTHY ✓" >> $REPORT_FILE
|
||||
else
|
||||
echo "Status: UNHEALTHY ✗" >> $REPORT_FILE
|
||||
fi
|
||||
done
|
||||
|
||||
cat $REPORT_FILE
|
||||
}
|
||||
```
|
||||
|
||||
### 故障诊断
|
||||
```bash
|
||||
# 诊断 Webhook 问题
|
||||
diagnose_webhook() {
|
||||
WEBHOOK_ID=$1
|
||||
|
||||
echo "=== Webhook Diagnosis ==="
|
||||
echo "Webhook ID: $WEBHOOK_ID"
|
||||
echo
|
||||
|
||||
# 1. 检查 Webhook 是否存在
|
||||
echo "1. Checking webhook existence..."
|
||||
if ! gitlink-cli webhook +info --id $WEBHOOK_ID >/dev/null 2>&1; then
|
||||
echo " ✗ Webhook not found"
|
||||
return 1
|
||||
fi
|
||||
echo " ✓ Webhook exists"
|
||||
|
||||
# 2. 获取 Webhook 配置
|
||||
echo "2. Webhook configuration:"
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID
|
||||
|
||||
# 3. 测试网络连通性
|
||||
echo "3. Testing network connectivity..."
|
||||
WEBHOOK_URL=$(gitlink-cli webhook +info --id $WEBHOOK_ID --format json | jq -r '.data.hook_url')
|
||||
if curl -s -o /dev/null -w "%{http_code}" "$WEBHOOK_URL" | grep -q "200\|301\|302"; then
|
||||
echo " ✓ URL is accessible"
|
||||
else
|
||||
echo " ✗ URL is not accessible"
|
||||
fi
|
||||
|
||||
# 4. 测试 Webhook
|
||||
echo "4. Testing webhook delivery..."
|
||||
if gitlink-cli webhook +test --id $WEBHOOK_ID; then
|
||||
echo " ✓ Webhook test successful"
|
||||
else
|
||||
echo " ✗ Webhook test failed"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **测试限制**: 测试事件不会触发实际的业务逻辑,仅验证连通性
|
||||
2. **请求格式**: 测试请求的格式与真实事件略有不同
|
||||
3. **响应时间**: Webhook 应在 10 秒内响应,否则超时
|
||||
4. **重试机制**: 测试失败不会触发 GitLink 的重试机制
|
||||
5. **权限要求**: 需要仓库管理员权限
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 场景1: 验证新 Webhook
|
||||
```bash
|
||||
# 创建 Webhook 后验证配置
|
||||
WEBHOOK_ID=$(gitlink-cli webhook +create \
|
||||
--url https://ci.example.com/webhook \
|
||||
--events push,pull_request \
|
||||
--format json | jq -r '.data.id')
|
||||
|
||||
# 测试各种事件类型
|
||||
for event in push pull_request; do
|
||||
echo "Testing $event event..."
|
||||
gitlink-cli webhook +test --id $WEBHOOK_ID --event $event
|
||||
done
|
||||
```
|
||||
|
||||
### 场景2: 故障排查
|
||||
```bash
|
||||
# Webhook 未触发时进行测试
|
||||
# 1. 检查 Webhook 是否激活
|
||||
gitlink-cli webhook +info --id 456
|
||||
|
||||
# 2. 测试 Webhook 连通性
|
||||
gitlink-cli webhook +test --id 456
|
||||
|
||||
# 3. 查看详细错误信息
|
||||
gitlink-cli webhook +test --id 456 --debug
|
||||
```
|
||||
|
||||
### 场景3: 批量验证
|
||||
```bash
|
||||
# 验证所有 Webhook 在服务器迁移后是否正常
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
echo "Testing webhook $id..."
|
||||
if ! gitlink-cli webhook +test --id $id; then
|
||||
echo "WARNING: Webhook $id needs attention"
|
||||
# 可以在这里添加自动修复逻辑
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **避免敏感数据**: 测试事件可能包含真实数据,注意隐私保护
|
||||
2. **测试频率**: 不要过于频繁测试,避免对服务器造成压力
|
||||
3. **错误信息**: 测试失败时的错误信息可能暴露系统细节
|
||||
4. **访问控制**: 确保测试 URL 只暴露必要的信息
|
||||
|
||||
## 相关命令
|
||||
|
||||
- `webhook +list` - 列出所有 Webhook
|
||||
- `webhook +info` - 查看 Webhook 详情
|
||||
- `webhook +create` - 创建新 Webhook
|
||||
- `webhook +update` - 更新 Webhook 配置
|
||||
- `webhook +events` - 查看支持的事件类型
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
# gitlink-cli webhook +update
|
||||
|
||||
更新现有 Webhook 的配置。
|
||||
|
||||
## 命令格式
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +update \
|
||||
--owner OWNER \
|
||||
--repo REPO \
|
||||
--id WEBHOOK_ID \
|
||||
[--url URL] \
|
||||
[--events EVENTS] \
|
||||
[--active ACTIVE] \
|
||||
[--content_type CONTENT_TYPE] \
|
||||
[--secret SECRET] \
|
||||
[--description DESCRIPTION]
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 短参数 | 说明 | 是否必须 | 默认值 |
|
||||
|------|--------|------|----------|--------|
|
||||
| `--owner` | `-o` | 仓库所有者 | 否 | 自动从 git remote 解析 |
|
||||
| `--repo` | `-r` | 仓库名称 | 否 | 自动从 git remote 解析 |
|
||||
| `--id` | `-i` | Webhook ID | **是** | - |
|
||||
| `--url` | `-u` | 新的 Webhook URL | 否 | 不修改 |
|
||||
| `--events` | `-e` | 新的触发事件 | 否 | 不修改 |
|
||||
| `--active` | - | 是否激活 | 否 | 不修改 |
|
||||
| `--content_type` | - | 内容类型 | 否 | 不修改 |
|
||||
| `--secret` | - | 新的密钥 | 否 | 不修改 |
|
||||
| `--description` | `-d` | 新的描述 | 否 | 不修改 |
|
||||
|
||||
## 返回值
|
||||
|
||||
### 成功返回
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"id": "456",
|
||||
"hook_url": "https://new-url.example.com/webhook",
|
||||
"events": ["push", "pull_request", "issue"],
|
||||
"is_active": false,
|
||||
"content_type": "json",
|
||||
"description": "Updated webhook description",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-02T12:00:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"identity": "user:myuser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误返回
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": 404,
|
||||
"message": "Webhook not found",
|
||||
"suggestion": "Please check the webhook ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 更新 URL
|
||||
```bash
|
||||
# 修改 Webhook 回调地址
|
||||
gitlink-cli webhook +update --owner myuser --repo myrepo --id 456 --url https://new-url.example.com/webhook
|
||||
|
||||
# 在 git 仓库目录中更新
|
||||
gitlink-cli webhook +update --id 456 --url https://new-url.example.com/webhook
|
||||
```
|
||||
|
||||
### 更新事件
|
||||
```bash
|
||||
# 添加更多事件类型
|
||||
gitlink-cli webhook +update --id 456 --events push,pull_request,issue,issue_comment
|
||||
|
||||
# 减少事件类型(只监听 push)
|
||||
gitlink-cli webhook +update --id 456 --events push
|
||||
```
|
||||
|
||||
### 激活/停用 Webhook
|
||||
```bash
|
||||
# 停用 Webhook
|
||||
gitlink-cli webhook +update --id 456 --active false
|
||||
|
||||
# 重新激活 Webhook
|
||||
gitlink-cli webhook +update --id 456 --active true
|
||||
```
|
||||
|
||||
### 更新密钥
|
||||
```bash
|
||||
# 更新 Webhook 密钥(推荐定期轮换)
|
||||
gitlink-cli webhook +update --id 456 --secret new-secret-key-2024
|
||||
```
|
||||
|
||||
### 更新描述
|
||||
```bash
|
||||
# 更新 Webhook 描述
|
||||
gitlink-cli webhook +update --id 456 --description "Updated for new CI/CD pipeline"
|
||||
```
|
||||
|
||||
### 批量更新多个属性
|
||||
```bash
|
||||
# 同时更新多个属性
|
||||
gitlink-cli webhook +update \
|
||||
--id 456 \
|
||||
--url https://new-url.example.com/webhook \
|
||||
--events push,pull_request,issue \
|
||||
--secret new-secret \
|
||||
--description "Comprehensive webhook update"
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
#### 1. Webhook 不存在
|
||||
```bash
|
||||
Error: [404] Webhook not found
|
||||
```
|
||||
**原因**: 指定的 Webhook ID 不存在
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 先列出所有 Webhook 找到正确 ID
|
||||
gitlink-cli webhook +list
|
||||
```
|
||||
|
||||
#### 2. 无效的事件类型
|
||||
```bash
|
||||
Error: no valid events specified
|
||||
```
|
||||
**原因**: 指定了不支持的事件类型
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查看支持的事件
|
||||
gitlink-cli webhook +events
|
||||
```
|
||||
|
||||
#### 3. 权限不足
|
||||
```bash
|
||||
Error: [403] You don't have permission to update webhooks
|
||||
```
|
||||
**原因**: 用户不是仓库管理员
|
||||
**解决方案**: 确认您有仓库管理员权限
|
||||
|
||||
#### 4. 没有指定更新字段
|
||||
```bash
|
||||
Error: no fields specified for update
|
||||
```
|
||||
**原因**: 没有提供任何要更新的字段
|
||||
**解决方案**: 至少指定一个要更新的字段
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 密钥轮换
|
||||
```bash
|
||||
# 定期更新密钥(建议每3个月)
|
||||
gitlink-cli webhook +update --id 456 --secret $(openssl rand -hex 32)
|
||||
```
|
||||
|
||||
### 2. 临时停用
|
||||
```bash
|
||||
# 临时停用 Webhook 进行维护
|
||||
gitlink-cli webhook +update --id 456 --active false
|
||||
|
||||
# 维护完成后重新激活
|
||||
gitlink-cli webhook +update --id 456 --active true
|
||||
```
|
||||
|
||||
### 3. 渐进式更新
|
||||
```bash
|
||||
# 先测试新配置
|
||||
gitlink-cli webhook +update --id 456 --url https://new-url.example.com/webhook --active false
|
||||
gitlink-cli webhook +test --id 456
|
||||
|
||||
# 确认无误后激活
|
||||
gitlink-cli webhook +update --id 456 --active true
|
||||
```
|
||||
|
||||
## AI Agent 使用建议
|
||||
|
||||
### 批量更新 Webhook
|
||||
```bash
|
||||
# 为所有 Webhook 添加新事件
|
||||
for id in $(gitlink-cli webhook +list --format json | jq -r '.data.webhooks[].id'); do
|
||||
# 获取当前事件
|
||||
current_events=$(gitlink-cli webhook +info --id $id --format json | jq -r '.data.events | join(",")')
|
||||
# 添加新事件
|
||||
gitlink-cli webhook +update --id $id --events "$current_events,issue_comment"
|
||||
done
|
||||
```
|
||||
|
||||
### 验证更新
|
||||
```bash
|
||||
# 更新后立即验证
|
||||
WEBHOOK_ID=456
|
||||
gitlink-cli webhook +update --id $WEBHOOK_ID --url $NEW_URL
|
||||
gitlink-cli webhook +info --id $WEBHOOK_ID
|
||||
gitlink-cli webhook +test --id $WEBHOOK_ID
|
||||
```
|
||||
|
||||
### 检查更新前后差异
|
||||
```bash
|
||||
# 查看更新前后配置差异
|
||||
BEFORE=$(gitlink-cli webhook +info --id 456 --format json)
|
||||
gitlink-cli webhook +update --id 456 --url $NEW_URL
|
||||
AFTER=$(gitlink-cli webhook +info --id 456 --format json)
|
||||
|
||||
# 对比差异(需要 jq 工具)
|
||||
echo "Before:" && echo "$BEFORE" | jq '.data'
|
||||
echo "After:" && echo "$AFTER" | jq '.data'
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **密钥轮换**: 定期更新 Webhook 密钥,建议每3个月一次
|
||||
2. **测试新配置**: 更新重要配置前先停用,测试后再激活
|
||||
3. **备份配置**: 更新前记录原配置,便于回滚
|
||||
4. **权限验证**: 确保只有授权用户能修改 Webhook
|
||||
5. **审计日志**: 记录所有 Webhook 配置变更
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **部分更新**: 只更新指定的字段,未指定的字段保持不变
|
||||
2. **ID 不变**: 更新不会改变 Webhook ID
|
||||
3. **立即生效**: 更新后立即生效,除非停用 Webhook
|
||||
4. **测试验证**: 建议更新后测试 Webhook 是否正常工作
|
||||
5. **权限要求**: 需要仓库管理员权限
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 场景1: 迁移 Webhook 到新服务器
|
||||
```bash
|
||||
# 更新 Webhook URL 到新服务器
|
||||
gitlink-cli webhook +update --id 456 --url https://new-server.example.com/webhook
|
||||
# 测试新地址
|
||||
gitlink-cli webhook +test --id 456
|
||||
```
|
||||
|
||||
### 场景2: 调整事件监听
|
||||
```bash
|
||||
# 原来只监听 push,现在增加 PR 监听
|
||||
gitlink-cli webhook +update --id 456 --events push,pull_request
|
||||
```
|
||||
|
||||
### 场景3: 安全密钥轮换
|
||||
```bash
|
||||
# 定期更新密钥提高安全性
|
||||
NEW_SECRET=$(openssl rand -hex 32)
|
||||
gitlink-cli webhook +update --id 456 --secret "$NEW_SECRET"
|
||||
# 更新接收服务器的密钥配置
|
||||
# 然后测试
|
||||
gitlink-cli webhook +test --id 456
|
||||
```
|
||||
|
||||
### 场景4: 临时维护
|
||||
```bash
|
||||
# 临时停用 Webhook
|
||||
gitlink-cli webhook +update --id 456 --active false --description "Maintenance in progress"
|
||||
|
||||
# 维护完成后重新激活
|
||||
gitlink-cli webhook +update --id 456 --active true --description "Production webhook"
|
||||
```
|
||||
|
||||
## 相关命令
|
||||
|
||||
- `webhook +list` - 列出所有 Webhook
|
||||
- `webhook +create` - 创建新 Webhook
|
||||
- `webhook +info` - 查看 Webhook 详情
|
||||
- `webhook +test` - 测试 Webhook
|
||||
|
|
@ -0,0 +1,959 @@
|
|||
# Wiki 工作流示例
|
||||
|
||||
本文档提供了使用 `gitlink-cli wiki` 命令的完整工作流示例,涵盖从简单到复杂的各种场景。
|
||||
|
||||
## 目录
|
||||
|
||||
- [基础工作流](#基础工作流)
|
||||
- [项目文档初始化](#项目文档初始化)
|
||||
- [文档维护工作流](#文档维护工作流)
|
||||
- [批量操作](#批量操作)
|
||||
- [AI Agent 集成](#ai-agent-集成)
|
||||
- [故障排除](#故障排除)
|
||||
|
||||
---
|
||||
|
||||
## 基础工作流
|
||||
|
||||
### 工作流 1: 创建单个 Wiki 页面
|
||||
|
||||
**场景**: 为项目创建首页
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 1. 创建首页
|
||||
gitlink-cli wiki +create --title "Home" --content '# Project Home
|
||||
|
||||
## Overview
|
||||
This project is a CLI tool for GitLink platform.
|
||||
|
||||
## Features
|
||||
- Repository management
|
||||
- Issue tracking
|
||||
- Pull requests
|
||||
|
||||
## Documentation
|
||||
- [Getting Started](Getting-Started)
|
||||
- [API Reference](API-Reference)
|
||||
- [Contributing](Contributing)
|
||||
|
||||
## Support
|
||||
- [FAQ](FAQ)
|
||||
- [Contact Us](Contact-Us)'
|
||||
|
||||
# 2. 验证创建结果
|
||||
gitlink-cli wiki +view --title "Home"
|
||||
|
||||
# 3. 列出所有页面
|
||||
gitlink-cli wiki +list
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- 创建了标题为 "Home" 的 Wiki 页面
|
||||
- 页面包含导航链接和项目概述
|
||||
- 可通过 `wiki +list` 和 `wiki +view` 验证
|
||||
|
||||
---
|
||||
|
||||
## 项目文档初始化
|
||||
|
||||
### 工作流 2: 创建完整项目文档结构
|
||||
|
||||
**场景**: 为新项目创建完整的 Wiki 文档体系
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# init-project-wiki.sh
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
echo "=== Initializing Project Wiki ==="
|
||||
|
||||
# 1. 创建首页
|
||||
echo "Creating Home page..."
|
||||
gitlink-cli wiki +create --title "Home" --content '# Project Documentation
|
||||
|
||||
Welcome to the project documentation!
|
||||
|
||||
## Quick Links
|
||||
- 📚 [Getting Started](Getting-Started) - New user guide
|
||||
- 📖 [API Reference](API-Reference) - API documentation
|
||||
- 🤝 [Contributing](Contributing) - Contribution guide
|
||||
- ❓ [FAQ](FAQ) - Frequently asked questions
|
||||
|
||||
## Overview
|
||||
This project provides a comprehensive CLI tool for GitLink platform management.
|
||||
|
||||
## Status
|
||||
- Version: 1.0.0
|
||||
- License: MIT
|
||||
- Support: See [Contact Us](Contact-Us)'
|
||||
|
||||
# 2. 创建入门指南
|
||||
echo "Creating Getting Started guide..."
|
||||
gitlink-cli wiki +create --title "Getting-Started" --content '# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 14+
|
||||
- GitLink account
|
||||
|
||||
### Install via npm
|
||||
\`\`\`bash
|
||||
npm install -g gitlink-cli
|
||||
\`\`\`
|
||||
|
||||
### Install from source
|
||||
\`\`\`bash
|
||||
git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git
|
||||
cd gitlink-cli
|
||||
make install
|
||||
\`\`\`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Initialize config
|
||||
\`\`\`bash
|
||||
gitlink-cli config init
|
||||
\`\`\`
|
||||
|
||||
### Login
|
||||
\`\`\`bash
|
||||
gitlink-cli auth login
|
||||
\`\`\`
|
||||
|
||||
## Verify Installation
|
||||
\`\`\`bash
|
||||
gitlink-cli --version
|
||||
gitlink-cli user +me
|
||||
\`\`\`'
|
||||
|
||||
# 3. 创建 API 文档
|
||||
echo "Creating API Reference..."
|
||||
gitlink-cli wiki +create --title "API-Reference" --content '# API Reference
|
||||
|
||||
## Repository Operations
|
||||
|
||||
### List repositories
|
||||
\`\`\`bash
|
||||
gitlink-cli repo +list
|
||||
\`\`\`
|
||||
|
||||
### Create repository
|
||||
\`\`\`bash
|
||||
gitlink-cli repo +create -n my-project -d "Project description"
|
||||
\`\`\`
|
||||
|
||||
## Issue Operations
|
||||
|
||||
### List issues
|
||||
\`\`\`bash
|
||||
gitlink-cli issue +list --owner user --repo project
|
||||
\`\`\`
|
||||
|
||||
### Create issue
|
||||
\`\`\`bash
|
||||
gitlink-cli issue +create -t "Bug title" -b "Bug description"
|
||||
\`\`\`
|
||||
|
||||
## Pull Request Operations
|
||||
|
||||
### List PRs
|
||||
\`\`\`bash
|
||||
gitlink-cli pr +list --owner user --repo project
|
||||
\`\`\`
|
||||
|
||||
### Create PR
|
||||
\`\`\`bash
|
||||
gitlink-cli pr +create --head feature --base main -t "Feature title"
|
||||
\`\`\`'
|
||||
|
||||
# 4. 创建贡献指南
|
||||
echo "Creating Contributing guide..."
|
||||
gitlink-cli wiki +create --title "Contributing" --content '# Contributing
|
||||
|
||||
Thank you for your interest in contributing!
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### Report Bugs
|
||||
Create an issue with the bug report template.
|
||||
|
||||
### Submit Changes
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Setup Development Environment
|
||||
\`\`\`bash
|
||||
git clone https://www.gitlink.org.cn/YOUR_USERNAME/gitlink-cli.git
|
||||
cd gitlink-cli
|
||||
make install
|
||||
\`\`\`
|
||||
|
||||
### Run Tests
|
||||
\`\`\`bash
|
||||
make test
|
||||
\`\`\`
|
||||
|
||||
### Code Style
|
||||
- Follow Go conventions
|
||||
- Add tests for new features
|
||||
- Update documentation
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### PR Title Format
|
||||
- \`feat: add new feature\`
|
||||
- \`fix: fix bug description\`
|
||||
- \`docs: update documentation\`
|
||||
|
||||
### PR Description
|
||||
Include:
|
||||
- Problem statement
|
||||
- Solution approach
|
||||
- Testing performed
|
||||
- Related issues'
|
||||
|
||||
# 5. 创建 FAQ
|
||||
echo "Creating FAQ..."
|
||||
gitlink-cli wiki +create --title "FAQ" --content '# Frequently Asked Questions
|
||||
|
||||
## General Questions
|
||||
|
||||
### Q: What is gitlink-cli?
|
||||
A: GitLink CLI is a command-line tool for managing GitLink platform resources.
|
||||
|
||||
### Q: How do I install gitlink-cli?
|
||||
A: Run \`npm install -g gitlink-cli\` or build from source.
|
||||
|
||||
## Authentication
|
||||
|
||||
### Q: How do I authenticate?
|
||||
A: Run \`gitlink-cli auth login\` and provide your credentials.
|
||||
|
||||
### Q: How long does the token last?
|
||||
A: Tokens expire after 7 days. Re-authenticate when expired.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Q: Command not found
|
||||
A: Ensure npm global bin is in your PATH: \`export PATH=\$PATH:\$(npm config get prefix)/bin\`
|
||||
|
||||
### Q: Permission denied
|
||||
A: Run \`gitlink-cli auth login\` to re-authenticate.
|
||||
|
||||
## More Help
|
||||
- See [Getting Started](Getting-Started)
|
||||
- Check [API Reference](API-Reference)
|
||||
- Contact: [Contact Us](Contact-Us)'
|
||||
|
||||
# 6. 创建联系我们页面
|
||||
echo "Creating Contact Us page..."
|
||||
gitlink-cli wiki +create --title "Contact-Us" --content '# Contact Us
|
||||
|
||||
## Get Help
|
||||
|
||||
### Documentation
|
||||
- [Getting Started](Getting-Started)
|
||||
- [API Reference](API-Reference)
|
||||
- [FAQ](FAQ)
|
||||
|
||||
### Community
|
||||
- Forum: [GitLink Forum](https://forum.gitlink.org.cn)
|
||||
- Chat: [Gitter Channel](https://gitter.im/gitlink-cli)
|
||||
|
||||
### Report Issues
|
||||
- Bug Reports: [Issue Tracker](https://www.gitlink.org.cn/Gitlink/gitlink-cli/issues)
|
||||
- Feature Requests: [Issue Tracker](https://www.gitlink.org.cn/Gitlink/gitlink-cli/issues)
|
||||
|
||||
## Development Team
|
||||
|
||||
### Maintainers
|
||||
- @maintainer1 - Project lead
|
||||
- @maintainer2 - Core development
|
||||
|
||||
### Contributors
|
||||
See [CONTRIBUTORS.md](https://www.gitlink.org.cn/Gitlink/gitlink-cli/blob/master/CONTRIBUTORS.md)
|
||||
|
||||
## License
|
||||
This project is licensed under the MulanPSL-2.0 License.
|
||||
|
||||
## Acknowledgments
|
||||
Thanks to all contributors who have helped improve this project!'
|
||||
|
||||
echo "=== Wiki Initialization Complete ==="
|
||||
echo "Created 6 documentation pages:"
|
||||
gitlink-cli wiki +list
|
||||
```
|
||||
|
||||
**关键特性**:
|
||||
- ✅ 创建了完整的文档结构
|
||||
- ✅ 页面之间有交叉引用链接
|
||||
- ✅ 包含代码示例和命令
|
||||
- ✅ 覆盖了项目的所有主要方面
|
||||
|
||||
---
|
||||
|
||||
## 文档维护工作流
|
||||
|
||||
### 工作流 3: 更新文档内容
|
||||
|
||||
**场景**: 文档需要定期更新以反映项目变化
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# update-documentation.sh
|
||||
|
||||
page_title="API-Reference"
|
||||
backup_file="wiki-backup-$(date '+%Y%m%d-%H%M%S').md"
|
||||
|
||||
echo "=== Safe Wiki Update Workflow ==="
|
||||
|
||||
# 1. 备份当前内容
|
||||
echo "Step 1: Backing up current content..."
|
||||
gitlink-cli wiki +view --title "$page_title" --format json | \
|
||||
jq -r ".data.content_decoded" > "$backup_file"
|
||||
echo "✓ Backup saved: $backup_file"
|
||||
|
||||
# 2. 显示当前内容预览
|
||||
echo ""
|
||||
echo "Step 2: Current content preview:"
|
||||
head -n 10 "$backup_file"
|
||||
echo "..."
|
||||
|
||||
# 3. 编辑内容(使用临时文件)
|
||||
temp_file="temp-wiki-update.md"
|
||||
cp "$backup_file" "$temp_file"
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Edit the content in: $temp_file"
|
||||
echo "Press Enter when done editing..."
|
||||
read
|
||||
|
||||
# 4. 确认更新
|
||||
echo ""
|
||||
echo "Step 4: Review changes:"
|
||||
echo "--- Old content (first 5 lines) ---"
|
||||
head -n 5 "$backup_file"
|
||||
echo "--- New content (first 5 lines) ---"
|
||||
head -n 5 "$temp_file"
|
||||
echo "---"
|
||||
|
||||
read -p "Apply changes? (y/N) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# 5. 执行更新
|
||||
echo "Step 5: Applying update..."
|
||||
gitlink-cli wiki +update --title "$page_title" --file "$temp_file"
|
||||
|
||||
# 6. 验证结果
|
||||
echo "Step 6: Verifying update..."
|
||||
gitlink-cli wiki +view --title "$page_title" --format json | \
|
||||
jq -r ".data.content_decoded" > "updated-content.md"
|
||||
|
||||
if diff -q "$temp_file" "updated-content.md" >/dev/null; then
|
||||
echo "✓ Update successful!"
|
||||
rm "$temp_file" "updated-content.md"
|
||||
else
|
||||
echo "✗ Update verification failed!"
|
||||
echo "Backup available at: $backup_file"
|
||||
fi
|
||||
else
|
||||
echo "✗ Update cancelled."
|
||||
echo "Backup available at: $backup_file"
|
||||
rm "$temp_file"
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工作流 4: 追加更新日志
|
||||
|
||||
**场景**: 在文档末尾追加更新日志
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# append-changelog.sh
|
||||
|
||||
page_title="Home"
|
||||
changelog_content="
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### v$(date '+%Y.%m.%d')
|
||||
- Updated documentation structure
|
||||
- Added new examples
|
||||
- Fixed typos and errors
|
||||
- Improved API references"
|
||||
|
||||
echo "=== Appending Changelog to $page_title ==="
|
||||
|
||||
# 1. 查看当前末尾内容
|
||||
echo "Current page ending:"
|
||||
gitlink-cli wiki +view --title "$page_title" --format json | \
|
||||
jq -r ".data.content_decoded" | tail -n 5
|
||||
|
||||
# 2. 确认追加
|
||||
echo ""
|
||||
echo "Content to append:"
|
||||
echo "$changelog_content"
|
||||
|
||||
read -p "Append changelog? (y/N) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# 3. 追加内容
|
||||
gitlink-cli wiki +update --title "$page_title" --add "$changelog_content"
|
||||
echo "✓ Changelog appended successfully!"
|
||||
|
||||
# 4. 验证
|
||||
echo ""
|
||||
echo "Updated page ending:"
|
||||
gitlink-cli wiki +view --title "$page_title" --format json | \
|
||||
jq -r ".data.content_decoded" | tail -n 10
|
||||
else
|
||||
echo "✗ Append cancelled."
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 批量操作
|
||||
|
||||
### 工作流 5: 从本地目录批量导入 Wiki
|
||||
|
||||
**场景**: 将本地的 Markdown 文档批量导入到 Wiki
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# batch-import-wiki.sh
|
||||
|
||||
wiki_docs_dir="./wiki-docs"
|
||||
backup_dir="wiki-import-backup-$(date '+%Y%m%d-%H%M%S')"
|
||||
|
||||
echo "=== Batch Wiki Import ==="
|
||||
|
||||
# 1. 检查目录
|
||||
if [ ! -d "$wiki_docs_dir" ]; then
|
||||
echo "Error: Directory '$wiki_docs_dir' not found."
|
||||
echo "Please create it and add your Markdown files."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 创建备份目录
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
# 3. 统计文件
|
||||
md_files=("$wiki_docs_dir"/*.md)
|
||||
total_files=${#md_files[@]}
|
||||
|
||||
echo "Found $total_files Markdown files in '$wiki_docs_dir'"
|
||||
|
||||
# 4. 遍历导入
|
||||
success_count=0
|
||||
skip_count=0
|
||||
error_count=0
|
||||
|
||||
for mdfile in "${md_files[@]}"; do
|
||||
# 从文件名提取标题(去掉 .md 后缀)
|
||||
filename=$(basename "$mdfile")
|
||||
title="${filename%.md}"
|
||||
|
||||
echo ""
|
||||
echo "Processing: $filename"
|
||||
|
||||
# 检查页面是否已存在
|
||||
if gitlink-cli wiki +view --title "$title" >/dev/null 2>&1; then
|
||||
echo " ⚠️ Page '$title' already exists. Skipping."
|
||||
((skip_count++))
|
||||
|
||||
# 备份现有页面
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "$backup_dir/$filename"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 创建新页面
|
||||
if gitlink-cli wiki +create --title "$title" --file "$mdfile" 2>/dev/null; then
|
||||
echo " ✓ Created: $title"
|
||||
((success_count++))
|
||||
else
|
||||
echo " ✗ Failed: $title"
|
||||
((error_count++))
|
||||
|
||||
# 失败时备份文件
|
||||
cp "$mdfile" "$backup_dir/"
|
||||
fi
|
||||
done
|
||||
|
||||
# 5. 显示统计
|
||||
echo ""
|
||||
echo "=== Import Summary ==="
|
||||
echo "Total files: $total_files"
|
||||
echo "✓ Created: $success_count"
|
||||
echo "⚠️ Skipped: $skip_count (already exists)"
|
||||
echo "✗ Failed: $error_count"
|
||||
|
||||
if [ $error_count -gt 0 ]; then
|
||||
echo ""
|
||||
echo "Failed files backed up to: $backup_dir"
|
||||
fi
|
||||
|
||||
# 6. 列出当前所有页面
|
||||
echo ""
|
||||
echo "Current Wiki pages:"
|
||||
gitlink-cli wiki +list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工作流 6: 批量导出 Wiki 为本地文件
|
||||
|
||||
**场景**: 将所有 Wiki 页面导出为本地 Markdown 文件
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# batch-export-wiki.sh
|
||||
|
||||
export_dir="wiki-export-$(date '+%Y%m%d-%H%M%S')"
|
||||
|
||||
echo "=== Batch Wiki Export ==="
|
||||
|
||||
# 1. 创建导出目录
|
||||
mkdir -p "$export_dir"
|
||||
echo "Export directory: $export_dir"
|
||||
|
||||
# 2. 获取所有页面标题
|
||||
titles=$(gitlink-cli wiki +list --format json | jq -r '.data[].title')
|
||||
total_titles=$(echo "$titles" | wc -l)
|
||||
|
||||
echo "Found $total_titles Wiki pages"
|
||||
|
||||
# 3. 遍历导出
|
||||
success_count=0
|
||||
error_count=0
|
||||
|
||||
for title in $titles; do
|
||||
# 清理文件名(替换特殊字符)
|
||||
filename=$(echo "$title" | sed 's/[^a-zA-Z0-9_-]/_/g').md
|
||||
|
||||
echo "Exporting: $title -> $filename"
|
||||
|
||||
# 导出页面内容
|
||||
if gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r '.data.content_decoded' > "$export_dir/$filename" 2>/dev/null; then
|
||||
echo " ✓ Exported: $filename"
|
||||
((success_count++))
|
||||
else
|
||||
echo " ✗ Failed: $title"
|
||||
((error_count++))
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. 显示统计
|
||||
echo ""
|
||||
echo "=== Export Summary ==="
|
||||
echo "Total pages: $total_titles"
|
||||
echo "✓ Exported: $success_count"
|
||||
echo "✗ Failed: $error_count"
|
||||
|
||||
# 5. 创建索引文件
|
||||
echo "# Wiki Export Index" > "$export_dir/README.md"
|
||||
echo "" >> "$export_dir/README.md"
|
||||
echo "Export Date: $(date)" >> "$export_dir/README.md"
|
||||
echo "" >> "$export_dir/README.md"
|
||||
echo "## Pages" >> "$export_dir/README.md"
|
||||
echo "" >> "$export_dir/README.md"
|
||||
|
||||
for title in $titles; do
|
||||
filename=$(echo "$title" | sed 's/[^a-zA-Z0-9_-]/_/g').md
|
||||
echo "- [$title]($filename)" >> "$export_dir/README.md"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✓ Index created: $export_dir/README.md"
|
||||
echo "Export completed: $export_dir"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工作流 7: 批量重命名页面
|
||||
|
||||
**场景**: 统一 Wiki 页面命名规范
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# batch-rename-wiki.sh
|
||||
|
||||
# 定义重命名规则(旧标题 -> 新标题)
|
||||
declare -A rename_rules=(
|
||||
["api"]="API-Reference"
|
||||
["getting started"]="Getting-Started"
|
||||
["user guide"]="User-Guide"
|
||||
["faq"]="FAQ"
|
||||
["home"]="Home"
|
||||
)
|
||||
|
||||
echo "=== Batch Wiki Rename ==="
|
||||
|
||||
# 1. 显示重命名计划
|
||||
echo "Planned renames:"
|
||||
for old_title in "${!rename_rules[@]}"; do
|
||||
new_title="${rename_rules[$old_title]}"
|
||||
echo " '$old_title' -> '$new_title'"
|
||||
done
|
||||
|
||||
# 2. 确认执行
|
||||
read -p "Proceed with renaming? (yes/NO) " -r
|
||||
echo
|
||||
|
||||
if [[ ! "$REPLY" == "yes" ]]; then
|
||||
echo "✗ Renaming cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 3. 执行重命名
|
||||
success_count=0
|
||||
skip_count=0
|
||||
error_count=0
|
||||
|
||||
for old_title in "${!rename_rules[@]}"; do
|
||||
new_title="${rename_rules[$old_title]}"
|
||||
|
||||
echo ""
|
||||
echo "Renaming: '$old_title' -> '$new_title'"
|
||||
|
||||
# 检查旧页面是否存在
|
||||
if ! gitlink-cli wiki +view --title "$old_title" >/dev/null 2>&1; then
|
||||
echo " ⚠️ Old page '$old_title' not found. Skipping."
|
||||
((skip_count++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查新页面是否已存在
|
||||
if gitlink-cli wiki +view --title "$new_title" >/dev/null 2>&1; then
|
||||
echo " ⚠️ Target page '$new_title' already exists. Skipping."
|
||||
((skip_count++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# 执行重命名
|
||||
if gitlink-cli wiki +update --page "$old_title" --title "$new_title" 2>/dev/null; then
|
||||
echo " ✓ Renamed successfully"
|
||||
((success_count++))
|
||||
else
|
||||
echo " ✗ Rename failed"
|
||||
((error_count++))
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. 显示统计
|
||||
echo ""
|
||||
echo "=== Rename Summary ==="
|
||||
echo "Total planned: ${#rename_rules[@]}"
|
||||
echo "✓ Renamed: $success_count"
|
||||
echo "⚠️ Skipped: $skip_count"
|
||||
echo "✗ Failed: $error_count"
|
||||
|
||||
# 5. 列出当前所有页面
|
||||
echo ""
|
||||
echo "Current Wiki pages:"
|
||||
gitlink-cli wiki +list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI Agent 集成
|
||||
|
||||
### 工作流 8: AI Agent 自动文档管理
|
||||
|
||||
**场景**: AI Agent 自动维护项目文档
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# ai_wiki_manager.py - AI Agent Wiki 管理示例
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
class WikiManager:
|
||||
"""GitLink Wiki 管理器 - 为 AI Agent 设计"""
|
||||
|
||||
def __init__(self, owner, repo):
|
||||
self.owner = owner
|
||||
self.repo = repo
|
||||
self.base_cmd = ["gitlink-cli", "--owner", owner, "--repo", repo]
|
||||
|
||||
def run_command(self, command):
|
||||
"""执行 gitlink-cli 命令并返回结果"""
|
||||
try:
|
||||
full_cmd = self.base_cmd + command
|
||||
result = subprocess.run(
|
||||
full_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {' '.join(full_cmd)}")
|
||||
print(f"Error: {e.stderr}")
|
||||
return None
|
||||
|
||||
def list_pages(self):
|
||||
"""列出所有 Wiki 页面"""
|
||||
output = self.run_command(["wiki", "+list", "--format", "json"])
|
||||
if output:
|
||||
data = json.loads(output)
|
||||
return data.get("data", [])
|
||||
return []
|
||||
|
||||
def get_page_content(self, title):
|
||||
"""获取指定页面的内容"""
|
||||
output = self.run_command(
|
||||
["wiki", "+view", "--title", title, "--format", "json"]
|
||||
)
|
||||
if output:
|
||||
data = json.loads(output)
|
||||
return data.get("data", {}).get("content_decoded", "")
|
||||
return None
|
||||
|
||||
def create_page(self, title, content):
|
||||
"""创建新页面"""
|
||||
# 创建临时文件
|
||||
temp_file = f"/tmp/wiki_{title}.md"
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
# 从文件创建
|
||||
result = self.run_command(
|
||||
["wiki", "+create", "--title", title, "--file", temp_file]
|
||||
)
|
||||
|
||||
# 清理临时文件
|
||||
os.remove(temp_file)
|
||||
return result is not None
|
||||
|
||||
def update_page(self, title, content, mode="cover"):
|
||||
"""更新页面内容
|
||||
|
||||
Args:
|
||||
title: 页面标题
|
||||
content: 新内容
|
||||
mode: 更新模式 ("cover" 或 "add")
|
||||
"""
|
||||
temp_file = f"/tmp/wiki_update_{title}.md"
|
||||
with open(temp_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
if mode == "cover":
|
||||
result = self.run_command(
|
||||
["wiki", "+update", "--title", title, "--file", temp_file]
|
||||
)
|
||||
else: # add mode
|
||||
result = self.run_command(
|
||||
["wiki", "+update", "--title", title, "--add", "",
|
||||
"--file", temp_file]
|
||||
)
|
||||
|
||||
os.remove(temp_file)
|
||||
return result is not None
|
||||
|
||||
def delete_page(self, title):
|
||||
"""删除页面"""
|
||||
result = self.run_command(["wiki", "+delete", "--title", title])
|
||||
return result is not None
|
||||
|
||||
def search_in_pages(self, keyword):
|
||||
"""在所有页面中搜索关键词"""
|
||||
pages = self.list_pages()
|
||||
results = []
|
||||
|
||||
for page in pages:
|
||||
title = page.get("title", "")
|
||||
content = self.get_page_content(title)
|
||||
|
||||
if content and keyword.lower() in content.lower():
|
||||
results.append({
|
||||
"title": title,
|
||||
"url": page.get("sub_url", ""),
|
||||
"preview": self.get_preview(content, keyword)
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def get_preview(self, content, keyword, context=50):
|
||||
"""获取关键词周围的预览文本"""
|
||||
index = content.lower().find(keyword.lower())
|
||||
if index == -1:
|
||||
return ""
|
||||
|
||||
start = max(0, index - context)
|
||||
end = min(len(content), index + len(keyword) + context)
|
||||
return content[start:end]
|
||||
|
||||
|
||||
# AI Agent 使用示例
|
||||
def ai_agent_example():
|
||||
"""AI Agent 自动维护文档的示例"""
|
||||
|
||||
# 初始化 Wiki 管理器
|
||||
wiki = WikiManager("Gitlink", "forgeplus")
|
||||
|
||||
print("=== AI Agent Wiki Manager ===")
|
||||
|
||||
# 1. 检查文档完整性
|
||||
print("\n1. Checking documentation completeness...")
|
||||
required_pages = ["Home", "Getting-Started", "API-Reference", "FAQ"]
|
||||
current_pages = [p.get("title") for p in wiki.list_pages()]
|
||||
|
||||
missing_pages = set(required_pages) - set(current_pages)
|
||||
if missing_pages:
|
||||
print(f" ⚠️ Missing pages: {missing_pages}")
|
||||
# AI Agent 可以自动创建缺失的页面
|
||||
else:
|
||||
print(" ✓ All required pages exist")
|
||||
|
||||
# 2. 检查过时内容
|
||||
print("\n2. Checking for outdated content...")
|
||||
outdated_keywords = ["version 0.9", "deprecated", "coming soon"]
|
||||
for keyword in outdated_keywords:
|
||||
results = wiki.search_in_pages(keyword)
|
||||
if results:
|
||||
print(f" ⚠️ Found '{keyword}' in:")
|
||||
for result in results:
|
||||
print(f" - {result['title']}")
|
||||
# AI Agent 可以标记这些页面需要更新
|
||||
|
||||
# 3. 自动更新版本信息
|
||||
print("\n3. Auto-updating version information...")
|
||||
home_content = wiki.get_page_content("Home")
|
||||
if home_content and "Version: 1.0.0" in home_content:
|
||||
new_version = "1.0.1"
|
||||
updated_content = home_content.replace("1.0.0", new_version)
|
||||
if wiki.update_page("Home", updated_content, "cover"):
|
||||
print(f" ✓ Updated version to {new_version}")
|
||||
|
||||
# 4. 生成统计报告
|
||||
print("\n4. Generating statistics...")
|
||||
pages = wiki.list_pages()
|
||||
total_pages = len(pages)
|
||||
|
||||
print(f" Total pages: {total_pages}")
|
||||
print(f" Last updated: {datetime.now().strftime('%Y-%m-%d')}")
|
||||
|
||||
# 计算每个页面的字符数
|
||||
for page in pages:
|
||||
title = page['title']
|
||||
content = wiki.get_page_content(title)
|
||||
if content:
|
||||
char_count = len(content)
|
||||
print(f" - {title}: {char_count} characters")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ai_agent_example()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 工作流 9: 常见问题诊断
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# wiki-diagnose.sh - Wiki 问题诊断工具
|
||||
|
||||
echo "=== Wiki Diagnostic Tool ==="
|
||||
|
||||
# 1. 检查认证状态
|
||||
echo "1. Checking authentication..."
|
||||
if gitlink-cli auth status 2>/dev/null | grep -q "Logged in"; then
|
||||
echo " ✓ Authentication OK"
|
||||
else
|
||||
echo " ✗ Authentication failed"
|
||||
echo " Solution: Run 'gitlink-cli auth login'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 检查网络连接
|
||||
echo "2. Checking network connectivity..."
|
||||
if curl -s -o /dev/null -w "%{http_code}" https://www.gitlink.org.cn | grep -q "200\|301\|302"; then
|
||||
echo " ✓ Network connectivity OK"
|
||||
else
|
||||
echo " ✗ Network connectivity failed"
|
||||
echo " Solution: Check your internet connection"
|
||||
fi
|
||||
|
||||
# 3. 检查 Gateway API 可用性
|
||||
echo "3. Checking Gateway API..."
|
||||
if curl -s -o /dev/null -w "%{http_code}" https://gateway.gitlink.org.cn/api | grep -q "200\|301\|302"; then
|
||||
echo " ✓ Gateway API available"
|
||||
else
|
||||
echo " ✗ Gateway API unavailable"
|
||||
echo " Solution: Gateway API may be down, try again later"
|
||||
fi
|
||||
|
||||
# 4. 检查项目权限
|
||||
echo "4. Checking project permissions..."
|
||||
if gitlink-cli repo +info >/dev/null 2>&1; then
|
||||
echo " ✓ Project access OK"
|
||||
else
|
||||
echo " ✗ Project access failed"
|
||||
echo " Solution: Check if --owner and --repo are correct"
|
||||
fi
|
||||
|
||||
# 5. 测试 Wiki 功能
|
||||
echo "5. Testing Wiki functionality..."
|
||||
page_count=$(gitlink-cli wiki +list --format json 2>/dev/null | jq '.meta.total_count // 0')
|
||||
if [ "$page_count" -ge 0 ]; then
|
||||
echo " ✓ Wiki功能正常 (当前页面数: $page_count)"
|
||||
else
|
||||
echo " ✗ Wiki功能异常"
|
||||
echo " Solution: Wiki may not be enabled for this project"
|
||||
fi
|
||||
|
||||
# 6. 显示诊断总结
|
||||
echo ""
|
||||
echo "=== Diagnostic Summary ==="
|
||||
echo "如果以上检查都通过,Wiki 功能应该可以正常使用。"
|
||||
echo "如果仍有问题,请检查:"
|
||||
echo " 1. 页面标题是否正确(区分大小写)"
|
||||
echo " 2. 是否有足够的权限操作 Wiki"
|
||||
echo " 3. 网络连接是否稳定"
|
||||
echo " 4. GitLink 平台是否正常运行"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
本文档提供了从基础到高级的 Wiki 工作流示例,涵盖:
|
||||
|
||||
- ✅ **基础操作**: 创建、查看、更新、删除
|
||||
- ✅ **项目初始化**: 完整的文档结构建立
|
||||
- ✅ **文档维护**: 安全的更新和追加工作流
|
||||
- ✅ **批量处理**: 导入、导出、重命名批量操作
|
||||
- ✅ **AI 集成**: Python 实现的自动化管理
|
||||
- ✅ **故障排除**: 诊断和问题解决
|
||||
|
||||
这些工作流可以直接使用或根据具体需求调整。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [gitlink-wiki](../SKILL.md) — Wiki 功能总览
|
||||
- [wiki +list](../references/wiki-list.md) — 列出页面
|
||||
- [wiki +create](../references/wiki-create.md) — 创建页面
|
||||
- [wiki +update](../references/wiki-update.md) — 更新页面
|
||||
- [wiki +delete](../references/wiki-delete.md) — 删除页面
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
# wiki +create
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **⚠️ 写入操作** — 执行前必须确认用户意图。
|
||||
|
||||
创建新的 Wiki 页面。支持直接提供内容或从文件读取。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 创建简单页面(使用 --content)
|
||||
gitlink-cli wiki +create --title "Home" --content "# Welcome\n\nThis is the home page."
|
||||
|
||||
# 创建页面(从文件读取)
|
||||
gitlink-cli wiki +create --title "API Reference" --file api.md
|
||||
|
||||
# 创建页面并添加提交消息
|
||||
gitlink-cli wiki +create --title "Getting Started" \
|
||||
--content "# Getting Started\n\n..." \
|
||||
--message "Initial documentation"
|
||||
|
||||
# 创建多行内容页面
|
||||
gitlink-cli wiki +create --title "Guide" --content "# User Guide
|
||||
|
||||
## Installation
|
||||
Run the following command:
|
||||
|
||||
\`\`\`bash
|
||||
npm install
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
\`\`\`bash
|
||||
npm start
|
||||
\`\`\`"
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | Short | 必填 | 说明 |
|
||||
|------|-------|------|------|
|
||||
| `--title` | `-t` | **是** | Wiki 页面标题 |
|
||||
| `--content` | `-c` | **是*** | Wiki 页面内容(纯文本,与 `--file` 二选一) |
|
||||
| `--file` | `-f` | **是*** | 从文件读取内容(与 `--content` 二选一) |
|
||||
| `--message` | `-m` | 否 | 提交消息(可选) |
|
||||
| `--owner` | | 否 | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | | 否 | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | | 否 | 开启调试输出 |
|
||||
|
||||
* `--content` 和 `--file` 必须指定其中一个
|
||||
|
||||
## 返回字段
|
||||
|
||||
### Table 格式
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| `title` | 创建的页面标题 |
|
||||
| `message` | 操作结果消息 |
|
||||
|
||||
### JSON 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"title": "Home",
|
||||
"message": "Wiki page created successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the page title and content with the user.
|
||||
2. **Check** if the page already exists (optional, use `wiki +view`).
|
||||
3. **Execute** `gitlink-cli wiki +create --title "<title>" --content "<content>"`.
|
||||
4. **Report** the creation result and page URL.
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent before executing.
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
POST https://gateway.gitlink.org.cn/api/wiki/open/createWiki
|
||||
Body: {
|
||||
"owner": "...",
|
||||
"repo": "...",
|
||||
"projectId": 123,
|
||||
"pageName": "<title>",
|
||||
"title": "<title>",
|
||||
"content_base64": "<base64-encoded-content>",
|
||||
"message": "<optional-message>"
|
||||
}
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. CLI 获取 `project_id`
|
||||
2. 将内容 Base64 编码为 `content_base64`
|
||||
3. 调用 Gateway API 创建页面
|
||||
4. 返回创建结果
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1: 创建首页
|
||||
|
||||
当用户请求"创建项目首页"时:
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +create --title "Home" \
|
||||
--content "# Project Home
|
||||
|
||||
## Overview
|
||||
This project is a CLI tool for GitLink platform.
|
||||
|
||||
## Features
|
||||
- Repository management
|
||||
- Issue tracking
|
||||
- Pull requests
|
||||
|
||||
## Getting Started
|
||||
See the [Getting Started](Getting-Started) page."
|
||||
```
|
||||
|
||||
### 场景 2: 从现有文件创建
|
||||
|
||||
```bash
|
||||
# 从 README.md 创建 Wiki
|
||||
gitlink-cli wiki +create --title "Home" --file README.md
|
||||
|
||||
# 从多个文件创建多个页面
|
||||
gitlink-cli wiki +create --title "API Reference" --file docs/api.md
|
||||
gitlink-cli wiki +create --title "User Guide" --file docs/guide.md
|
||||
```
|
||||
|
||||
### 场景 3: 创建代码文档
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +create --title "CLI Reference" --content "# CLI Commands
|
||||
|
||||
## Repository Commands
|
||||
\`\`\`bash
|
||||
gitlink-cli repo +list
|
||||
gitlink-cli repo +create -n my-project
|
||||
\`\`\`
|
||||
|
||||
## Issue Commands
|
||||
\`\`\`bash
|
||||
gitlink-cli issue +list
|
||||
gitlink-cli issue +create -t \"Bug: ...\"
|
||||
\`\`\`"
|
||||
```
|
||||
|
||||
### 场景 4: 批量创建 Wiki 页面
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 从 docs/ 目录批量创建 Wiki 页面
|
||||
|
||||
for mdfile in docs/*.md; do
|
||||
# 从文件名提取标题(去掉 .md 后缀)
|
||||
title=$(basename "$mdfile" .md)
|
||||
|
||||
echo "Creating Wiki page: $title"
|
||||
gitlink-cli wiki +create --title "$title" --file "$mdfile"
|
||||
done
|
||||
```
|
||||
|
||||
## 内容编码
|
||||
|
||||
### Base64 自动处理
|
||||
|
||||
**无需手动编码** — CLI 自动处理:
|
||||
|
||||
```bash
|
||||
# CLI 会自动将以下内容 Base64 编码
|
||||
gitlink-cli wiki +create --title "Test" --content "Hello, World!"
|
||||
|
||||
# 等效于手动编码(不推荐)
|
||||
gitlink-cli api POST "https://gateway.gitlink.org.cn/api/wiki/open/createWiki" \
|
||||
--body '{
|
||||
"owner": "...",
|
||||
"repo": "...",
|
||||
"projectId": 123,
|
||||
"pageName": "Test",
|
||||
"title": "Test",
|
||||
"content_base64": "SGVsbG8sIFdvcmxkIQ=="
|
||||
}'
|
||||
```
|
||||
|
||||
### 多行内容处理
|
||||
|
||||
```bash
|
||||
# 方法 1: 使用 \n 换行
|
||||
gitlink-cli wiki +create --title "Test" \
|
||||
--content "Line 1\nLine 2\nLine 3"
|
||||
|
||||
# 方法 2: 使用 $'' 引号(支持 \n)
|
||||
gitlink-cli wiki +create --title "Test" --content $'Line 1\nLine 2\nLine 3'
|
||||
|
||||
# 方法 3: 从文件读取(推荐)
|
||||
cat << 'EOF' > temp.md
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
EOF
|
||||
gitlink-cli wiki +create --title "Test" --file temp.md
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 创建失败提示 "page already exists"?
|
||||
|
||||
**A:** 页面标题已存在。解决方法:
|
||||
```bash
|
||||
# 查看现有页面
|
||||
gitlink-cli wiki +list
|
||||
|
||||
# 使用不同的标题,或先删除现有页面
|
||||
gitlink-cli wiki +delete --title "Old Title"
|
||||
gitlink-cli wiki +create --title "New Title" --content "..."
|
||||
```
|
||||
|
||||
### Q: 内容显示格式错误?
|
||||
|
||||
**A:** 确保:
|
||||
1. Markdown 语法正确
|
||||
2. 使用 `\n` 表示换行(单行字符串)
|
||||
3. 或从文件读取(保留原始格式)
|
||||
|
||||
### Q: 如何创建包含代码块的页面?
|
||||
|
||||
**A:** 使用正确的 Markdown 语法:
|
||||
```bash
|
||||
gitlink-cli wiki +create --title "Code Examples" \
|
||||
--content '# Code Examples
|
||||
|
||||
## JavaScript
|
||||
\`\`\`javascript
|
||||
console.log("Hello");
|
||||
\`\`\`
|
||||
|
||||
## Python
|
||||
\`\`\`python
|
||||
print("Hello")
|
||||
\`\`\`'
|
||||
```
|
||||
|
||||
### Q: 支持哪些 Markdown 语法?
|
||||
|
||||
**A:** GitLink Wiki 支持:
|
||||
- 标题 (`#`, `##`, `###`)
|
||||
- 列表(有序、无序)
|
||||
- 代码块(```)
|
||||
- 链接 (`[text](url)`)
|
||||
- 图片 (``)
|
||||
- 表格
|
||||
- 粗体、斜体、引用
|
||||
|
||||
### Q: 可以创建 HTML 内容吗?
|
||||
|
||||
**A:** GitLink Wiki 主要支持 Markdown,部分 HTML 可能被过滤。建议使用标准 Markdown 语法。
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `required flag --title is missing` | 未指定标题 | 添加 `--title "Page Title"` |
|
||||
| `--content or --file is required` | 未提供内容 | 添加 `--content "..."` 或 `--file file.md` |
|
||||
| `failed to read file` | 文件不存在或无权限 | 检查文件路径和权限 |
|
||||
| `page already exists` | 标题已存在 | 使用不同标题或先删除现有页面 |
|
||||
| `401 Unauthorized` | 未登录或 Token 过期 | 运行 `gitlink-cli auth login` |
|
||||
| `403 Forbidden` | 无权限创建 Wiki | 检查是否有项目写入权限 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 标题命名规范
|
||||
|
||||
```bash
|
||||
# 推荐:使用连字符连接单词
|
||||
"Getting-Started"
|
||||
"API-Reference"
|
||||
"User-Guide"
|
||||
|
||||
# 避免:空格和特殊字符
|
||||
"Getting Started" # 需要引号
|
||||
"API/Reference" # 斜杠可能被误解
|
||||
```
|
||||
|
||||
### 2. 内容模板
|
||||
|
||||
创建文档时使用标准模板:
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +create --title "Page Title" --content '# Page Title
|
||||
|
||||
## Overview
|
||||
Brief description of the page.
|
||||
|
||||
## Details
|
||||
Detailed content.
|
||||
|
||||
## Examples
|
||||
\`\`\`bash
|
||||
Example code
|
||||
\`\`\`
|
||||
|
||||
## See Also
|
||||
- [Related Page 1](Related-Page-1)
|
||||
- [Related Page 2](Related-Page-2)'
|
||||
```
|
||||
|
||||
### 3. 从文件创建
|
||||
|
||||
对于复杂内容,先创建文件再导入:
|
||||
|
||||
```bash
|
||||
# 1. 创建本地 Markdown 文件
|
||||
cat > home.md << 'EOF'
|
||||
# Home
|
||||
|
||||
Welcome to the project!
|
||||
EOF
|
||||
|
||||
# 2. 从文件创建 Wiki
|
||||
gitlink-cli wiki +create --title "Home" --file home.md
|
||||
|
||||
# 3. 清理临时文件
|
||||
rm home.md
|
||||
```
|
||||
|
||||
### 4. 批量创建工作流
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 批量创建项目文档
|
||||
|
||||
# 定义页面列表
|
||||
declare -A pages=(
|
||||
["Home"]="home.md"
|
||||
["Getting-Started"]="getting-started.md"
|
||||
["API-Reference"]="api.md"
|
||||
["FAQ"]="faq.md"
|
||||
)
|
||||
|
||||
# 遍历创建
|
||||
for title in "${!pages[@]}"; do
|
||||
file="${pages[$title]}"
|
||||
if [ -f "$file" ]; then
|
||||
echo "Creating: $title from $file"
|
||||
gitlink-cli wiki +create --title "$title" --file "$file"
|
||||
else
|
||||
echo "Warning: $file not found, skipping $title"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例:创建完整项目 Wiki
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 为新项目创建完整的 Wiki 文档结构
|
||||
|
||||
# 1. 创建首页
|
||||
gitlink-cli wiki +create --title "Home" --content '# Project Home
|
||||
|
||||
## Overview
|
||||
This is a demonstration project for gitlink-cli Wiki.
|
||||
|
||||
## Documentation
|
||||
- [Getting Started](Getting-Started)
|
||||
- [API Reference](API-Reference)
|
||||
- [Contributing](Contributing)
|
||||
|
||||
## Support
|
||||
- [FAQ](FAQ)
|
||||
- [Contact Us](Contact-Us)'
|
||||
|
||||
# 2. 创建入门指南
|
||||
gitlink-cli wiki +create --title "Getting-Started" --content '# Getting Started
|
||||
|
||||
## Installation
|
||||
\`\`\`bash
|
||||
npm install
|
||||
\`\`\`
|
||||
|
||||
## Configuration
|
||||
\`\`\`bash
|
||||
cp .env.example .env
|
||||
\`\`\`
|
||||
|
||||
## Running
|
||||
\`\`\`bash
|
||||
npm start
|
||||
\`\`\`'
|
||||
|
||||
# 3. 创建 API 文档
|
||||
gitlink-cli wiki +create --title "API-Reference" --content '# API Reference
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /api/users
|
||||
Get user information.
|
||||
|
||||
### POST /api/issues
|
||||
Create a new issue.
|
||||
|
||||
## Examples
|
||||
See the [Examples](Examples) page.'
|
||||
|
||||
echo "Wiki documentation structure created successfully!"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-wiki](../SKILL.md)
|
||||
- [wiki +update](wiki-update.md) — 更新 Wiki 页面
|
||||
- [wiki +delete](wiki-delete.md) — 删除 Wiki 页面
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md)
|
||||
|
|
@ -0,0 +1,539 @@
|
|||
# wiki +delete
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **⚠️ 危险操作** — 删除操作**无法撤销**,执行前必须确认用户意图。
|
||||
|
||||
删除指定的 Wiki 页面。⚠️ **此操作不可逆!**
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 删除 Wiki 页面
|
||||
gitlink-cli wiki +delete --title "Old Page"
|
||||
|
||||
# 删除指定仓库的 Wiki 页面
|
||||
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --title "Outdated"
|
||||
|
||||
# 删除前先确认(推荐)
|
||||
gitlink-cli wiki +view --title "Page to Delete" # 先查看内容
|
||||
gitlink-cli wiki +delete --title "Page to Delete" # 再删除
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | Short | 必填 | 说明 |
|
||||
|------|-------|------|------|
|
||||
| `--title` | `-t` | **是** | 要删除的 Wiki 页面标题 |
|
||||
| `--owner` | | 否 | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | | 否 | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | | 否 | 开启调试输出 |
|
||||
|
||||
## 返回字段
|
||||
|
||||
### Table 格式
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| `message` | 操作结果消息 |
|
||||
|
||||
### JSON 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"message": "Wiki page deleted successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the page title to delete.
|
||||
2. **Warning** that this operation is **irreversible**.
|
||||
3. **Optional**: View current content with `wiki +view` for final verification.
|
||||
4. **Execute** `gitlink-cli wiki +delete --title "<page title>"`.
|
||||
5. **Report** the deletion result.
|
||||
|
||||
> [!DANGER]
|
||||
> **删除操作无法撤销!** 建议执行前先备份内容:
|
||||
> ```bash
|
||||
> gitlink-cli wiki +view --title "Page" > backup.md
|
||||
> gitlink-cli wiki +delete --title "Page"
|
||||
> ```
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
DELETE https://gateway.gitlink.org.cn/api/wiki/open/deleteWiki
|
||||
Body: {
|
||||
"owner": "...",
|
||||
"repo": "...",
|
||||
"projectId": 123,
|
||||
"pageName": "<title>",
|
||||
"message": ""
|
||||
}
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. CLI 获取 `project_id`
|
||||
2. 调用 Gateway API 删除页面
|
||||
3. 验证删除是否成功(尝试获取页面)
|
||||
4. 返回删除结果
|
||||
|
||||
**删除验证逻辑**:
|
||||
- 如果删除 API 返回成功 → 删除成功
|
||||
- 如果删除 API 失败,尝试获取页面:
|
||||
- 页面不存在 → 删除成功
|
||||
- 页面仍存在 → 删除失败
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1: 删除过时文档
|
||||
|
||||
```bash
|
||||
# 查看过时内容
|
||||
gitlink-cli wiki +view --title "Old API Reference"
|
||||
|
||||
# 确认后删除
|
||||
gitlink-cli wiki +delete --title "Old API Reference"
|
||||
```
|
||||
|
||||
### 场景 2: 清理测试页面
|
||||
|
||||
```bash
|
||||
# 列出所有页面
|
||||
gitlink-cli wiki +list
|
||||
|
||||
# 删除测试页面
|
||||
gitlink-cli wiki +delete --title "Test Page 1"
|
||||
gitlink-cli wiki +delete --title "Test Page 2"
|
||||
gitlink-cli wiki +delete --title "Test Page 3"
|
||||
```
|
||||
|
||||
### 场景 3: 批量删除(谨慎!)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 批量删除包含特定关键词的页面
|
||||
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
# 删除包含 "Draft" 的页面
|
||||
if [[ "$title" == *"Draft"* ]]; then
|
||||
echo "Deleting draft page: $title"
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 场景 4: 删除前备份
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 安全删除工作流:先备份再删除
|
||||
|
||||
title="Page to Delete"
|
||||
|
||||
# 1. 备份内容
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "${title}-backup.md"
|
||||
|
||||
# 2. 确认删除
|
||||
read -p "Backup created at ${title}-backup.md. Delete now? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
echo "Page deleted. Backup saved at ${title}-backup.md"
|
||||
else
|
||||
echo "Deletion cancelled."
|
||||
fi
|
||||
```
|
||||
|
||||
### 场景 5: 条件删除
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 根据页面内容决定是否删除
|
||||
|
||||
title="Deprecated Feature"
|
||||
|
||||
# 获取页面内容
|
||||
content=$(gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded")
|
||||
|
||||
# 检查是否包含"已废弃"标记
|
||||
if [[ "$content" == *"此功能已废弃"* ]]; then
|
||||
echo "Page is deprecated. Deleting..."
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
else
|
||||
echo "Page is still active. Not deleting."
|
||||
fi
|
||||
```
|
||||
|
||||
## 删除验证
|
||||
|
||||
### 方法 1: 尝试查看页面
|
||||
|
||||
```bash
|
||||
# 删除后验证
|
||||
gitlink-cli wiki +delete --title "Test Page"
|
||||
|
||||
# 尝试查看(应该返回 404)
|
||||
gitlink-cli wiki +view --title "Test Page"
|
||||
# 预期输出:Error: 404 Not Found
|
||||
```
|
||||
|
||||
### 方法 2: 列出所有页面
|
||||
|
||||
```bash
|
||||
# 删除前列出
|
||||
gitlink-cli wiki +list
|
||||
# 包含: "Test Page"
|
||||
|
||||
gitlink-cli wiki +delete --title "Test Page"
|
||||
|
||||
# 删除后列出
|
||||
gitlink-cli wiki +list
|
||||
# 不包含: "Test Page"
|
||||
```
|
||||
|
||||
### 方法 3: 统计页面数量
|
||||
|
||||
```bash
|
||||
# 删除前
|
||||
before=$(gitlink-cli wiki +list --format json | jq ".meta.total_count")
|
||||
echo "Pages before: $before"
|
||||
|
||||
# 删除
|
||||
gitlink-cli wiki +delete --title "Old Page"
|
||||
|
||||
# 删除后
|
||||
after=$(gitlink-cli wiki +list --format json | jq ".meta.total_count")
|
||||
echo "Pages after: $after"
|
||||
echo "Deleted: $((before - after)) page(s)"
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 删除后能否恢复?
|
||||
|
||||
**A:** **不能**。GitLink Wiki 不提供版本历史或回收站功能。
|
||||
|
||||
**建议**:
|
||||
1. 删除前务必备份:`gitlink-cli wiki +view --title "Page" > backup.md`
|
||||
2. 考虑使用重命名代替删除:`gitlink-cli wiki +update --page "Old" --title "Old-Archived"`
|
||||
3. 如有备份,可重新创建:`gitlink-cli wiki +create --title "Page" --file backup.md`
|
||||
|
||||
### Q: 删除失败提示 "page not found"?
|
||||
|
||||
**A:** 页面不存在或已被删除。
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 查看现有页面
|
||||
gitlink-cli wiki +list
|
||||
|
||||
# 确认页面标题正确(区分大小写)
|
||||
gitlink-cli wiki +delete --title "Correct-Title"
|
||||
```
|
||||
|
||||
### Q: 删除后页面链接还能访问吗?
|
||||
|
||||
**A:** 访问已删除页面会返回 **404 Not Found**。
|
||||
|
||||
如果有外部链接指向该页面,需要:
|
||||
1. 更新外部链接
|
||||
2. 或创建同名新页面
|
||||
3. 或设置重定向(GitLink Wiki 不支持,需手动更新)
|
||||
|
||||
### Q: 能否批量删除所有页面?
|
||||
|
||||
**A:** **可以,但极其危险!**
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# ⚠️ 危险操作:删除所有 Wiki 页面
|
||||
|
||||
read -p "⚠️ This will delete ALL wiki pages. Continue? (yes/NO) " -r
|
||||
if [[ "$REPLY" == "yes" ]]; then
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
echo "Deleting: $title"
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
done
|
||||
echo "All pages deleted."
|
||||
else
|
||||
echo "Cancelled."
|
||||
fi
|
||||
```
|
||||
|
||||
### Q: 删除操作需要什么权限?
|
||||
|
||||
**A:** 需要:
|
||||
- **项目写入权限**(Maintainer 或 Owner 角色)
|
||||
- **有效的认证 Token**
|
||||
|
||||
如果权限不足:
|
||||
```bash
|
||||
# 403 Forbidden → 检查权限
|
||||
# 401 Unauthorized → 运行 gitlink-cli auth login
|
||||
```
|
||||
|
||||
### Q: 如何防止误删除?
|
||||
|
||||
**A:** 建议:
|
||||
1. **删除前备份**:总是先备份内容
|
||||
2. **使用别名**:创建安全删除别名
|
||||
3. **确认操作**:删除前再次确认
|
||||
4. **文档规范**:制定删除流程文档
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `required flag --title is missing` | 未指定页面标题 | 添加 `--title "Page Title"` |
|
||||
| `page not found` | 页面不存在 | 使用 `wiki +list` 查看可用页面 |
|
||||
| `401 Unauthorized` | 未登录或 Token 过期 | 运行 `gitlink-cli auth login` |
|
||||
| `403 Forbidden` | 无权限删除 Wiki | 检查是否有项目写入权限 |
|
||||
| `failed to delete` | 删除操作失败 | 检查网络连接和 API 可用性 |
|
||||
|
||||
## 安全措施
|
||||
|
||||
### 1. 删除前备份脚本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# safe-delete.sh - 安全删除 Wiki 页面
|
||||
|
||||
title="$1"
|
||||
|
||||
if [ -z "$title" ]; then
|
||||
echo "Usage: ./safe-delete.sh '<Page Title>'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 检查页面是否存在
|
||||
if ! gitlink-cli wiki +view --title "$title" >/dev/null 2>&1; then
|
||||
echo "Error: Page '$title' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 备份内容
|
||||
backup_file="${title}-backup-$(date '+%Y%m%d-%H%M%S').md"
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "$backup_file"
|
||||
|
||||
echo "✓ Backup created: $backup_file"
|
||||
|
||||
# 3. 显示预览
|
||||
echo ""
|
||||
echo "Page content preview:"
|
||||
head -n 10 "$backup_file"
|
||||
echo "..."
|
||||
|
||||
# 4. 确认删除
|
||||
read -p "Delete '$title' now? (yes/NO) " -r
|
||||
echo
|
||||
if [[ "$REPLY" == "yes" ]]; then
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Page deleted. Backup saved at: $backup_file"
|
||||
else
|
||||
echo "✗ Deletion failed. Backup available at: $backup_file"
|
||||
fi
|
||||
else
|
||||
echo "✗ Deletion cancelled. Backup saved at: $backup_file"
|
||||
fi
|
||||
```
|
||||
|
||||
### 2. 创建删除日志
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# deleted-pages.log - 记录所有删除操作
|
||||
|
||||
log_file="wiki-deletion-log.txt"
|
||||
title="$1"
|
||||
|
||||
# 记录删除操作
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deleted: $title" >> "$log_file"
|
||||
|
||||
# 执行删除
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
|
||||
echo "Deletion logged to: $log_file"
|
||||
```
|
||||
|
||||
### 3. 使用 Git 追踪删除
|
||||
|
||||
如果 Wiki 内容也在 Git 中管理:
|
||||
|
||||
```bash
|
||||
# 1. Git commit 删除前的状态
|
||||
git add docs/
|
||||
git commit -m "Backup before wiki deletion: $title"
|
||||
|
||||
# 2. 执行删除
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
|
||||
# 3. 记录删除
|
||||
echo "Deleted $title on $(date)" >> wiki-deletions.log
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 删除前检查清单
|
||||
|
||||
在删除 Wiki 页面前,确保:
|
||||
|
||||
- [ ] 已备份页面内容
|
||||
- [ ] 确认页面不再需要
|
||||
- [ ] 更新了相关链接
|
||||
- [ ] 通知了相关团队成员
|
||||
- [ ] 有权限执行删除操作
|
||||
|
||||
### 2. 替代删除的方案
|
||||
|
||||
**考虑使用重命名代替删除**:
|
||||
|
||||
```bash
|
||||
# 不删除,而是重命名为"已归档"
|
||||
gitlink-cli wiki +update --page "Old Feature" --title "Archived-Old-Feature"
|
||||
|
||||
# 或在页面顶部添加废弃标记
|
||||
gitlink-cli wiki +update --title "Old Feature" \
|
||||
--add "\n\n---\n\n⚠️ **此页面已废弃,请勿使用。**"
|
||||
```
|
||||
|
||||
### 3. 批量删除的安全流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 安全批量删除工作流
|
||||
|
||||
# 1. 列出待删除页面
|
||||
declare -a pages_to_delete=(
|
||||
"Test-Page-1"
|
||||
"Test-Page-2"
|
||||
"Draft-Document"
|
||||
)
|
||||
|
||||
# 2. 创建备份目录
|
||||
backup_dir="wiki-backup-$(date '+%Y%m%d-%H%M%S')"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
# 3. 备份所有页面
|
||||
for title in "${pages_to_delete[@]}"; do
|
||||
filename=$(echo "$title" | sed 's/[^a-zA-Z0-9_-]/_/g').md
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "$backup_dir/$filename"
|
||||
echo "Backed up: $title"
|
||||
done
|
||||
|
||||
# 4. 确认删除
|
||||
echo ""
|
||||
echo "Backup created at: $backup_dir"
|
||||
read -p "Delete all ${#pages_to_delete[@]} pages now? (yes/NO) " -r
|
||||
echo
|
||||
|
||||
if [[ "$REPLY" == "yes" ]]; then
|
||||
# 5. 执行删除
|
||||
for title in "${pages_to_delete[@]}"; do
|
||||
gitlink-cli wiki +delete --title "$title"
|
||||
echo "Deleted: $title"
|
||||
done
|
||||
echo "✓ All pages deleted. Backups saved at: $backup_dir"
|
||||
else
|
||||
echo "✗ Cancelled. Backups available at: $backup_dir"
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. 监控删除操作
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 监控 Wiki 页面数量变化
|
||||
|
||||
# 记录当前页面数量
|
||||
current_count=$(gitlink-cli wiki +list --format json | jq ".meta.total_count")
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Current page count: $current_count" >> wiki-monitor.log
|
||||
|
||||
# 如果页面数量异常减少,发出警告
|
||||
if [ -f "previous-count.txt" ]; then
|
||||
previous_count=$(cat previous-count.txt)
|
||||
if [ "$current_count" -lt "$previous_count" ]; then
|
||||
echo "⚠️ Warning: Page count decreased from $previous_count to $current_count"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: Count decreased: $previous_count -> $current_count" >> wiki-monitor.log
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$current_count" > previous-count.txt
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例:清理过时文档
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 完整的文档清理工作流
|
||||
|
||||
# 1. 定义过时页面列表
|
||||
declare -a outdated_pages=(
|
||||
"Legacy-API-v1"
|
||||
"Deprecated-Feature-X"
|
||||
"Old-Installation-Guide"
|
||||
)
|
||||
|
||||
# 2. 创建备份
|
||||
backup_dir="wiki-cleanup-backup-$(date '+%Y%m%d')"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
echo "=== Wiki Cleanup Process ==="
|
||||
echo "Backing up outdated pages..."
|
||||
|
||||
for title in "${outdated_pages[@]}"; do
|
||||
if gitlink-cli wiki +view --title "$title" >/dev/null 2>&1; then
|
||||
filename=$(echo "$title" | sed 's/[^a-zA-Z0-9_-]/_/g').md
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "$backup_dir/$filename"
|
||||
echo "✓ Backed up: $title"
|
||||
else
|
||||
echo "✗ Skipped (not found): $title"
|
||||
fi
|
||||
done
|
||||
|
||||
# 3. 确认删除
|
||||
echo ""
|
||||
echo "Outdated pages to delete:"
|
||||
printf " - %s\n" "${outdated_pages[@]}"
|
||||
echo ""
|
||||
read -p "Proceed with deletion? (yes/NO) " -r
|
||||
echo
|
||||
|
||||
if [[ "$REPLY" == "yes" ]]; then
|
||||
echo "Deleting outdated pages..."
|
||||
|
||||
for title in "${outdated_pages[@]}"; do
|
||||
if gitlink-cli wiki +delete --title "$title" 2>/dev/null; then
|
||||
echo "✓ Deleted: $title"
|
||||
else
|
||||
echo "✗ Failed (already deleted?): $title"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✓ Cleanup completed. Backups saved at: $backup_dir"
|
||||
echo "Remaining pages: $(gitlink-cli wiki +list --format json | jq '.meta.total_count')"
|
||||
else
|
||||
echo "✗ Cleanup cancelled. Backups available at: $backup_dir"
|
||||
fi
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-wiki](../SKILL.md)
|
||||
- [wiki +create](wiki-create.md) — 创建 Wiki 页面
|
||||
- [wiki +update](wiki-update.md) — 更新 Wiki 页面
|
||||
- [wiki +list](wiki-list.md) — 列出 Wiki 页面
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md)
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
# wiki +list
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
列出仓库的所有 Wiki 页面。返回页面标题、URL、更新时间等元信息。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 列出当前仓库的所有 Wiki 页面
|
||||
gitlink-cli wiki +list
|
||||
|
||||
# 列出指定仓库的 Wiki 页面
|
||||
gitlink-cli wiki +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 使用 JSON 格式输出
|
||||
gitlink-cli wiki +list --format json
|
||||
|
||||
# 查看 Wiki 页面总数
|
||||
gitlink-cli wiki +list --format json | jq ".data | length"
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--owner` | 否 | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否 | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 开启调试输出 |
|
||||
|
||||
## 返回字段
|
||||
|
||||
### Table 格式
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| `title` | Wiki 页面标题 |
|
||||
| `sub_url` | 页面访问路径(URL 编码) |
|
||||
| `updated_at` | 最后更新时间 |
|
||||
|
||||
### JSON 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": [
|
||||
{
|
||||
"title": "Home",
|
||||
"sub_url": "https://www.gitlink.org.cn/Gitlink/forgeplus/wiki/Home",
|
||||
"updated_at": "2026-06-01T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"title": "API-Reference",
|
||||
"sub_url": "https://www.gitlink.org.cn/Gitlink/forgeplus/wiki/API-Reference",
|
||||
"updated_at": "2026-06-01T11:15:00Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total_count": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Check** if `--owner` and `--repo` are provided or can be auto-resolved.
|
||||
2. **Execute** `gitlink-cli wiki +list`.
|
||||
3. **Display** the list of Wiki pages with titles and URLs.
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET https://gateway.gitlink.org.cn/api/wiki/open/wikiPages
|
||||
Query: owner={owner}&repo={repo}&projectId={project_id}
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- CLI 自动获取 `project_id`
|
||||
- 响应会被清理:移除 `wiki_clone_link` 字段
|
||||
- `sub_url` 会被 URL 解码以便阅读
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1: 发现项目文档
|
||||
|
||||
当用户询问"这个项目有什么文档"时:
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +list
|
||||
```
|
||||
|
||||
### 场景 2: 检查 Wiki 是否启用
|
||||
|
||||
当返回空列表时,说明项目未启用 Wiki 或没有创建任何页面。
|
||||
|
||||
### 场景 3: 批量处理所有 Wiki 页面
|
||||
|
||||
```bash
|
||||
# 获取所有 Wiki 页面标题
|
||||
titles=$(gitlink-cli wiki +list --format json | jq -r ".data[].title")
|
||||
|
||||
# 遍历每个页面
|
||||
for title in $titles; do
|
||||
echo "Processing: $title"
|
||||
gitlink-cli wiki +view --title "$title"
|
||||
done
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 返回空列表?
|
||||
|
||||
**A:** 可能原因:
|
||||
1. 项目没有创建任何 Wiki 页面
|
||||
2. 项目未启用 Wiki 功能
|
||||
3. `owner/repo` 指定错误
|
||||
|
||||
### Q: `sub_url` 字段是什么?
|
||||
|
||||
**A:** Wiki 页面的完整访问 URL,格式为:
|
||||
```
|
||||
https://www.gitlink.org.cn/{owner}/{repo}/wiki/{page_title}
|
||||
```
|
||||
|
||||
### Q: 如何获取页面总数?
|
||||
|
||||
**A:** 使用 JSON 格式并查看 `meta.total_count`:
|
||||
```bash
|
||||
gitlink-cli wiki +list --format json | jq ".meta.total_count"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-wiki](../SKILL.md)
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md)
|
||||
|
|
@ -0,0 +1,492 @@
|
|||
# wiki +update
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **⚠️ 写入操作** — 执行前必须确认用户意图。
|
||||
|
||||
更新现有 Wiki 页面。支持三种模式:**覆盖**、**追加**、**重命名**。
|
||||
|
||||
## 命令
|
||||
|
||||
### 覆盖模式(完全替换内容)
|
||||
|
||||
```bash
|
||||
# 覆盖整个页面内容
|
||||
gitlink-cli wiki +update --title "Home" --cover "# New Content\n\nThis replaces everything."
|
||||
|
||||
# 从文件覆盖
|
||||
gitlink-cli wiki +update --title "API Reference" --file new-api.md
|
||||
```
|
||||
|
||||
### 追加模式(在现有内容后追加)
|
||||
|
||||
```bash
|
||||
# 追加内容到现有页面
|
||||
gitlink-cli wiki +update --title "Home" --add "\n\n## New Section\n\nAdditional content."
|
||||
|
||||
# 从文件追加
|
||||
gitlink-cli wiki +update --title "Guide" --add "" --file appendix.md
|
||||
```
|
||||
|
||||
### 重命名模式(更改页面标题)
|
||||
|
||||
```bash
|
||||
# 重命名页面(保留原内容)
|
||||
gitlink-cli wiki +update --page "Old-Title" --title "New-Title"
|
||||
|
||||
# 重命名并更新内容
|
||||
gitlink-cli wiki +update --page "Old-Title" --title "New-Title" --cover "Updated content"
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | Short | 必填 | 说明 |
|
||||
|------|-------|------|------|
|
||||
| `--title` | `-t` | **是** | 目标页面标题(更新后的标题,用于重命名) |
|
||||
| `--page` | `-p` | 否 | 当前页面标题(用于查找和重命名,默认同 `--title`) |
|
||||
| `--cover` | `-c` | 否* | 覆盖整个页面内容(纯文本) |
|
||||
| `--add` | `-a` | 否* | 追加内容到现有页面(纯文本) |
|
||||
| `--file` | `-f` | 否 | 从文件读取内容(配合 `--cover` 或 `--add` 使用) |
|
||||
| `--message` | `-m` | 否 | 提交消息(可选) |
|
||||
| `--owner` | | 否 | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | | 否 | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | | 否 | 开启调试输出 |
|
||||
|
||||
* `--cover` 或 `--add` 必须指定其中一个,或都不指定(仅重命名)
|
||||
|
||||
## 返回字段
|
||||
|
||||
### Table 格式
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| `title` | 更新后的页面标题 |
|
||||
| `message` | 操作结果消息 |
|
||||
|
||||
### JSON 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"title": "Updated Title",
|
||||
"message": "Wiki page updated successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 三种更新模式
|
||||
|
||||
### 1. 覆盖模式 (`--cover`)
|
||||
|
||||
**完全替换页面内容**:
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +update --title "Home" --cover "# New Home Page
|
||||
|
||||
This completely replaces the old content."
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. 用户提供新内容
|
||||
2. CLI 直接用新内容替换整个页面
|
||||
3. 旧内容**完全丢失**
|
||||
|
||||
**使用场景**:
|
||||
- 完全重写页面
|
||||
- 修正错误内容
|
||||
- 大规模内容更新
|
||||
|
||||
### 2. 追加模式 (`--add`)
|
||||
|
||||
**在现有内容基础上追加**:
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +update --title "Home" --add "\n\n## Changelog
|
||||
|
||||
### v1.0.0 (2026-06-01)
|
||||
- Initial release"
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. CLI 获取当前页面内容
|
||||
2. 将新内容追加到现有内容后
|
||||
3. 提交更新后的完整内容
|
||||
|
||||
**使用场景**:
|
||||
- 添加新章节
|
||||
- 追加更新日志
|
||||
- 补充补充说明
|
||||
|
||||
### 3. 重命名模式 (`--page` + `--title`)
|
||||
|
||||
**更改页面标题**:
|
||||
|
||||
```bash
|
||||
# 仅重命名(保留原内容)
|
||||
gitlink-cli wiki +update --page "Old-Title" --title "New-Title"
|
||||
|
||||
# 重命名并更新内容
|
||||
gitlink-cli wiki +update --page "Old-Title" --title "New-Title" --cover "Updated"
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. `--page` 指定当前页面标题(用于查找)
|
||||
2. `--title` 指定新标题
|
||||
3. 如果指定 `--cover` 或 `--add`,同时更新内容
|
||||
|
||||
**使用场景**:
|
||||
- 修正页面标题拼写
|
||||
- 调整命名规范
|
||||
- 页面重组
|
||||
|
||||
## Workflow
|
||||
|
||||
### 覆盖模式
|
||||
|
||||
1. **Confirm** the new content with the user.
|
||||
2. **Warning** that this will replace all existing content.
|
||||
3. **Execute** `gitlink-cli wiki +update --title "<title>" --cover "<new content>"`.
|
||||
4. **Report** the update result.
|
||||
|
||||
### 追加模式
|
||||
|
||||
1. **Confirm** the content to append.
|
||||
2. **Execute** `gitlink-cli wiki +update --title "<title>" --add "<content>"`.
|
||||
3. **Report** the update result.
|
||||
|
||||
### 重命名模式
|
||||
|
||||
1. **Confirm** the old title (`--page`) and new title (`--title`).
|
||||
2. **Execute** `gitlink-cli wiki +update --page "<old>" --title "<new>"`.
|
||||
3. **Report** the rename result.
|
||||
|
||||
> [!CAUTION]
|
||||
> **覆盖模式** 会完全替换页面内容,无法撤销!建议先使用 `wiki +view` 查看当前内容,必要时手动备份。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
PUT https://gateway.gitlink.org.cn/api/wiki/open/updateWiki
|
||||
Body: {
|
||||
"owner": "...",
|
||||
"repo": "...",
|
||||
"projectId": 123,
|
||||
"pageName": "<current-title>",
|
||||
"title": "<new-title>",
|
||||
"content_base64": "<base64-encoded-content>",
|
||||
"message": "<optional-message>"
|
||||
}
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. CLI 获取 `project_id`
|
||||
2. 如果是追加模式,先获取当前页面内容
|
||||
3. 将内容 Base64 编码
|
||||
4. 调用 Gateway API 更新页面
|
||||
5. 返回更新结果
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1: 修正文档错误
|
||||
|
||||
```bash
|
||||
# 查看当前内容
|
||||
gitlink-cli wiki +view --title "API Reference"
|
||||
|
||||
# 修正错误
|
||||
gitlink-cli wiki +update --title "API Reference" \
|
||||
--file corrected-api.md
|
||||
```
|
||||
|
||||
### 场景 2: 添加更新日志
|
||||
|
||||
```bash
|
||||
# 追加更新日志到首页
|
||||
gitlink-cli wiki +update --title "Home" --add '
|
||||
## Changelog
|
||||
|
||||
### v2.0.0 (2026-06-01)
|
||||
- Added new feature X
|
||||
- Fixed bug Y
|
||||
- Improved performance Z'
|
||||
```
|
||||
|
||||
### 场景 3: 重命名页面
|
||||
|
||||
```bash
|
||||
# 将 "api" 重命名为 "API Reference"
|
||||
gitlink-cli wiki +update --page "api" --title "API Reference"
|
||||
|
||||
# 重命名并更新内容
|
||||
gitlink-cli wiki +update --page "old-guide" --title "User-Guide" \
|
||||
--cover "# User Guide\n\nUpdated content"
|
||||
```
|
||||
|
||||
### 场景 4: 批量更新多个页面
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 批量更新所有页面的页脚
|
||||
|
||||
declare -A pages=(
|
||||
["Home"]="home.md"
|
||||
["API-Reference"]="api.md"
|
||||
["Guide"]="guide.md"
|
||||
)
|
||||
|
||||
for title in "${!pages[@]}"; do
|
||||
file="${pages[$title]}"
|
||||
|
||||
# 读取文件内容作为覆盖内容
|
||||
echo "Updating: $title from $file"
|
||||
gitlink-cli wiki +update --title "$title" --file "$file"
|
||||
done
|
||||
```
|
||||
|
||||
### 场景 5: 增量更新文档
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 为所有页面添加"最后更新"时间戳
|
||||
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
current_date=$(date "+%Y-%m-%d")
|
||||
|
||||
gitlink-cli wiki +update --title "$title" \
|
||||
--add "\n\n---\n\n*Last updated: $current_date*"
|
||||
done
|
||||
```
|
||||
|
||||
## 模式选择指南
|
||||
|
||||
### 何时使用覆盖模式?
|
||||
|
||||
✅ **使用覆盖模式**:
|
||||
- 完全重写页面内容
|
||||
- 修正严重错误
|
||||
- 大规模内容更新
|
||||
- 从文件导入新版本
|
||||
|
||||
❌ **避免使用覆盖模式**:
|
||||
- 只需添加小段内容
|
||||
- 需要保留部分现有内容
|
||||
- 不确定要修改的具体内容
|
||||
|
||||
### 何时使用追加模式?
|
||||
|
||||
✅ **使用追加模式**:
|
||||
- 添加新章节
|
||||
- 追加更新日志
|
||||
- 补充补充说明
|
||||
- 保持历史内容
|
||||
|
||||
❌ **避免使用追加模式**:
|
||||
- 需要修正现有内容
|
||||
- 页面内容过长
|
||||
- 需要结构性修改
|
||||
|
||||
### 何时使用重命名模式?
|
||||
|
||||
✅ **使用重命名模式**:
|
||||
- 修正拼写错误
|
||||
- 统一命名规范
|
||||
- 页面重组
|
||||
- 调整文档结构
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 覆盖模式能否撤销?
|
||||
|
||||
**A:** **不能**。覆盖模式会完全替换内容,无法自动撤销。
|
||||
|
||||
**建议**:
|
||||
1. 先使用 `wiki +view` 查看当前内容
|
||||
2. 必要时手动备份:`gitlink-cli wiki +view --title "Page" > backup.md`
|
||||
3. 再执行覆盖更新
|
||||
|
||||
### Q: 追加模式的内容位置?
|
||||
|
||||
**A:** 追加的内容会添加到现有内容的**末尾**。
|
||||
|
||||
如果需要精确控制位置:
|
||||
1. 先查看当前内容
|
||||
2. 手动编辑(合并旧内容 + 新内容)
|
||||
3. 使用覆盖模式更新
|
||||
|
||||
### Q: 重命名后旧标题还能访问吗?
|
||||
|
||||
**A:** **不能**。重命名后:
|
||||
- 旧标题页面不存在
|
||||
- 使用旧标题访问会返回 404
|
||||
- 需要更新所有指向旧页面的链接
|
||||
|
||||
### Q: 如何同时修改标题和内容?
|
||||
|
||||
**A:** 使用 `--page` + `--title` + `--cover`:
|
||||
```bash
|
||||
gitlink-cli wiki +update \
|
||||
--page "Old-Title" \
|
||||
--title "New-Title" \
|
||||
--cover "New content"
|
||||
```
|
||||
|
||||
### Q: 更新失败提示 "page not found"?
|
||||
|
||||
**A:** 可能原因:
|
||||
1. `--title` 指定的页面不存在
|
||||
2. 如果使用 `--page`,当前页面不存在
|
||||
3. `owner/repo` 指定错误
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 先列出所有页面
|
||||
gitlink-cli wiki +list
|
||||
|
||||
# 确认页面标题正确(区分大小写)
|
||||
gitlink-cli wiki +update --title "Correct-Title" --cover "..."
|
||||
```
|
||||
|
||||
### Q: 追加模式获取旧内容失败?
|
||||
|
||||
**A:** 可能原因:
|
||||
1. 页面不存在
|
||||
2. 网络问题
|
||||
3. 权限不足
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 检查页面是否存在
|
||||
gitlink-cli wiki +view --title "Page-Name"
|
||||
|
||||
# 如果页面不存在,先创建
|
||||
gitlink-cli wiki +create --title "Page-Name" --content "..."
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `required flag --title is missing` | 未指定目标标题 | 添加 `--title "Page Title"` |
|
||||
| `--cover or --file is required` | 未提供更新内容 | 添加 `--cover "..."` 或 `--file file.md` |
|
||||
| `failed to fetch current page content` | 追加模式下页面不存在 | 先创建页面或检查标题 |
|
||||
| `page not found` | 指定页面不存在 | 使用 `wiki +list` 查看可用页面 |
|
||||
| `401 Unauthorized` | 未登录或 Token 过期 | 运行 `gitlink-cli auth login` |
|
||||
| `403 Forbidden` | 无权限更新 Wiki | 检查是否有项目写入权限 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 更新前备份
|
||||
|
||||
```bash
|
||||
# 更新前先备份当前内容
|
||||
gitlink-cli wiki +view --title "Important Page" --format json | \
|
||||
jq -r ".data.content_decoded" > backup.md
|
||||
|
||||
# 然后执行更新
|
||||
gitlink-cli wiki +update --title "Important Page" --file new-content.md
|
||||
```
|
||||
|
||||
### 2. 验证更新结果
|
||||
|
||||
```bash
|
||||
# 更新后查看新内容
|
||||
gitlink-cli wiki +view --title "Page" --format json | \
|
||||
jq -r ".data.content_decoded"
|
||||
```
|
||||
|
||||
### 3. 使用文件进行复杂更新
|
||||
|
||||
```bash
|
||||
# 1. 导出当前内容
|
||||
gitlink-cli wiki +view --title "Page" --format json | \
|
||||
jq -r ".data.content_decoded" > temp.md
|
||||
|
||||
# 2. 手动编辑 temp.md
|
||||
|
||||
# 3. 更新回 Wiki
|
||||
gitlink-cli wiki +update --title "Page" --file temp.md
|
||||
|
||||
# 4. 清理
|
||||
rm temp.md
|
||||
```
|
||||
|
||||
### 4. 批量重命名规范
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 统一命名规范:将空格替换为连字符
|
||||
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
# 如果标题包含空格
|
||||
if [[ "$title" =~ " " ]]; then
|
||||
# 生成新标题(空格替换为连字符)
|
||||
new_title=$(echo "$title" | sed 's/ /-/g')
|
||||
|
||||
echo "Renaming: '$title' -> '$new_title'"
|
||||
gitlink-cli wiki +update --page "$title" --title "$new_title"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 5. 增量更新工作流
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 安全的追加模式工作流
|
||||
|
||||
title="Home"
|
||||
new_content="## New Section\n\nNew content here."
|
||||
|
||||
# 1. 先检查页面是否存在
|
||||
if gitlink-cli wiki +view --title "$title" >/dev/null 2>&1; then
|
||||
# 2. 追加内容
|
||||
gitlink-cli wiki +update --title "$title" --add "\n\n$new_content"
|
||||
echo "Content appended to $title"
|
||||
else
|
||||
# 3. 页面不存在,创建新页面
|
||||
gitlink-cli wiki +create --title "$title" --content "$new_content"
|
||||
echo "New page $title created"
|
||||
fi
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 示例:重构项目文档
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 文档重构工作流
|
||||
|
||||
# 1. 备份所有页面
|
||||
mkdir -p wiki-backup
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
filename=$(echo "$title" | sed 's/[^a-zA-Z0-9_-]/_/g').md
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "wiki-backup/$filename"
|
||||
echo "Backed up: $title -> $filename"
|
||||
done
|
||||
|
||||
# 2. 重命名页面(统一命名规范)
|
||||
gitlink-cli wiki +update --page "api" --title "API-Reference"
|
||||
gitlink-cli wiki +update --page "user guide" --title "User-Guide"
|
||||
|
||||
# 3. 更新首页内容
|
||||
gitlink-cli wiki +update --title "Home" --file new-home.md
|
||||
|
||||
# 4. 为所有页面添加页脚
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
gitlink-cli wiki +update --title "$title" \
|
||||
--add "\n\n---\n\n*Updated: $(date '+%Y-%m-%d')*"
|
||||
done
|
||||
|
||||
echo "Wiki restructuring completed!"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-wiki](../SKILL.md)
|
||||
- [wiki +create](wiki-create.md) — 创建 Wiki 页面
|
||||
- [wiki +view](wiki-view.md) — 查看 Wiki 页面
|
||||
- [wiki +delete](wiki-delete.md) — 删除 Wiki 页面
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md)
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
# wiki +view
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
查看指定 Wiki 页面的完整内容,包括 Markdown 源文本。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 查看 Wiki 页面内容
|
||||
gitlink-cli wiki +view --title "Home"
|
||||
|
||||
# 查看指定仓库的 Wiki 页面
|
||||
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --title "API-Reference"
|
||||
|
||||
# 使用 JSON 格式查看(包含 base64 和 decoded 内容)
|
||||
gitlink-cli wiki +view --title "Home" --format json
|
||||
|
||||
# 将 Wiki 内容保存到文件
|
||||
gitlink-cli wiki +view --title "Home" --format json | jq -r ".data.content_decoded" > home.md
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | Short | 必填 | 说明 |
|
||||
|------|-------|------|------|
|
||||
| `--title` | `-t` | **是** | Wiki 页面标题(区分大小写) |
|
||||
| `--owner` | | 否 | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | | 否 | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | | 否 | 开启调试输出 |
|
||||
|
||||
## 返回字段
|
||||
|
||||
### Table 格式
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| `title` | Wiki 页面标题 |
|
||||
| `content` | 页面内容(自动解码后的文本) |
|
||||
| `updated_at` | 最后更新时间 |
|
||||
|
||||
### JSON 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"title": "Home",
|
||||
"content_base64": "I0hvbWUKCisqKldlbGNvbWUgdG8gdGhlIHByb2plY3QgIWJqKio=",
|
||||
"content_decoded": "# Home\n\n**Welcome to the project!**\n\n## Getting Started\n...",
|
||||
"updated_at": "2026-06-01T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `content_base64` - 原始 Base64 编码内容(API 返回)
|
||||
- `content_decoded` - 自动解码后的文本内容(CLI 提供)
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Confirm** the page title with the user.
|
||||
2. **Execute** `gitlink-cli wiki +view --title "<page title>"`.
|
||||
3. **Display** the page content (auto-decoded).
|
||||
4. **Optional**: Save content to file if requested.
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET https://gateway.gitlink.org.cn/api/wiki/open/getWiki
|
||||
Query: owner={owner}&repo={repo}&projectId={project_id}&pageName={title}
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. CLI 获取 `project_id`
|
||||
2. 调用 Gateway API 获取页面
|
||||
3. 解码 `content_base64` 为 `content_decoded`
|
||||
4. 返回解码后的内容
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景 1: 查看单个页面
|
||||
|
||||
当用户询问"查看 API 文档页面"时:
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +view --title "API Reference"
|
||||
```
|
||||
|
||||
### 场景 2: 导出 Wiki 页面
|
||||
|
||||
```bash
|
||||
# 导出为 Markdown 文件
|
||||
gitlink-cli wiki +view --title "Home" --format json | \
|
||||
jq -r ".data.content_decoded" > home.md
|
||||
```
|
||||
|
||||
### 场景 3: 批量导出所有 Wiki 页面
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 导出所有 Wiki 页面为 Markdown 文件
|
||||
|
||||
titles=$(gitlink-cli wiki +list --format json | jq -r ".data[].title")
|
||||
|
||||
for title in $titles; do
|
||||
# 清理文件名(替换空格和特殊字符)
|
||||
filename=$(echo "$title" | sed 's/[^a-zA-Z0-9_-]/_/g').md
|
||||
|
||||
echo "Exporting: $title -> $filename"
|
||||
gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" > "$filename"
|
||||
done
|
||||
```
|
||||
|
||||
### 场景 4: 检查页面是否存在
|
||||
|
||||
```bash
|
||||
# 检查页面是否存在(退出码 0=存在,非0=不存在)
|
||||
if gitlink-cli wiki +view --title "Some Page" >/dev/null 2>&1; then
|
||||
echo "Page exists"
|
||||
else
|
||||
echo "Page does not exist"
|
||||
fi
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 提示 "page not found"?
|
||||
|
||||
**A:** 可能原因:
|
||||
1. 页面标题不匹配(区分大小写)
|
||||
2. 页面不存在
|
||||
3. `owner/repo` 指定错误
|
||||
|
||||
**解决方法**:
|
||||
```bash
|
||||
# 先列出所有页面确认标题
|
||||
gitlink-cli wiki +list
|
||||
```
|
||||
|
||||
### Q: 内容显示为乱码?
|
||||
|
||||
**A:** 确保:
|
||||
1. 内容是有效的 UTF-8 编码
|
||||
2. 使用 `--format json` 查看 `content_decoded` 字段
|
||||
3. 终端支持 UTF-8 显示
|
||||
|
||||
### Q: 如何获取原始 Base64 内容?
|
||||
|
||||
**A:** 使用 JSON 格式查看 `content_base64` 字段:
|
||||
```bash
|
||||
gitlink-cli wiki +view --title "Home" --format json | jq ".data.content_base64"
|
||||
```
|
||||
|
||||
### Q: 支持哪些 Markdown 语法?
|
||||
|
||||
**A:** GitLink Wiki 支持 CommonMark 标准,包括:
|
||||
- 标题、列表、代码块
|
||||
- 链接、图片、表格
|
||||
- 粗体、斜体、引用
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| `required flag --title is missing` | 未指定页面标题 | 添加 `--title "Page Title"` |
|
||||
| `failed to fetch project_id` | 项目不存在或无权限 | 检查 `--owner` 和 `--repo` |
|
||||
| `failed to decode content` | Base64 解码失败 | 检查内容是否为有效 Base64 |
|
||||
| `404 Not Found` | 页面不存在 | 使用 `wiki +list` 查看可用页面 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 页面标题规范
|
||||
|
||||
使用一致的命名规范:
|
||||
```bash
|
||||
# 推荐:使用连字符
|
||||
"Getting-Started"
|
||||
"API-Reference"
|
||||
|
||||
# 避免:空格和特殊字符
|
||||
"Getting Started" # 需要引号
|
||||
"API/Reference" # 斜杠可能被误解为路径
|
||||
```
|
||||
|
||||
### 2. 内容验证
|
||||
|
||||
查看页面后验证内容完整性:
|
||||
```bash
|
||||
gitlink-cli wiki +view --title "Home" --format json | \
|
||||
jq -r ".data.content_decoded" | wc -l
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
|
||||
结合其他命令批量处理 Wiki:
|
||||
```bash
|
||||
# 查看所有页面的行数
|
||||
for title in $(gitlink-cli wiki +list --format json | jq -r ".data[].title"); do
|
||||
lines=$(gitlink-cli wiki +view --title "$title" --format json | \
|
||||
jq -r ".data.content_decoded" | wc -l)
|
||||
echo "$title: $lines lines"
|
||||
done
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-wiki](../SKILL.md)
|
||||
- [wiki +list](wiki-list.md) — 列出 Wiki 页面
|
||||
- [wiki +create](wiki-create.md) — 创建 Wiki 页面
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md)
|
||||
|
|
@ -0,0 +1,579 @@
|
|||
# AI Agent 自动化工作流完整示例
|
||||
|
||||
本文档展示如何组合使用多个 gitlink-cli 工作流,实现完整的 AI Agent 自动化项目管理。
|
||||
|
||||
## 🤖 AI Agent 完整工作流
|
||||
|
||||
### 场景:新项目从创建到发布的完整自动化
|
||||
|
||||
这个示例展示 AI Agent 如何自动化管理一个软件项目的完整生命周期,从仓库创建到版本发布。
|
||||
|
||||
## 工作流组合
|
||||
|
||||
### 1. 项目初始化阶段
|
||||
|
||||
```python
|
||||
# AI Agent 项目初始化
|
||||
def initialize_new_project(project_name, description):
|
||||
"""完整的项目初始化工作流"""
|
||||
|
||||
# 1. 创建仓库
|
||||
repo = create_repository(project_name, description)
|
||||
|
||||
# 2. 初始化项目结构
|
||||
setup_project_structure(repo)
|
||||
|
||||
# 3. 配置 CI/CD
|
||||
configure_ci_cd(repo)
|
||||
|
||||
# 4. 创建初始 Issue
|
||||
create_initial_issues(repo)
|
||||
|
||||
return repo
|
||||
|
||||
# 执行
|
||||
project = initialize_new_project(
|
||||
"my-awesome-project",
|
||||
"一个很棒的项目,用于演示自动化工作流"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 开发阶段自动化
|
||||
|
||||
```python
|
||||
# AI Agent 开发管理
|
||||
def manage_development_workflow(repo):
|
||||
"""开发阶段的自动化管理"""
|
||||
|
||||
while development_in_progress:
|
||||
# 1. 自动分类新 Issue
|
||||
triage_new_issues(repo)
|
||||
|
||||
# 2. 审查新的 PR
|
||||
review_pull_requests(repo)
|
||||
|
||||
# 3. 更新项目进度
|
||||
update_project_status(repo)
|
||||
|
||||
# 4. 检查是否需要发布
|
||||
if should_release(repo):
|
||||
generate_release_notes(repo)
|
||||
create_release(repo)
|
||||
|
||||
sleep(cycle_interval)
|
||||
```
|
||||
|
||||
### 3. Sprint 自动化
|
||||
|
||||
```python
|
||||
# AI Agent Sprint 管理
|
||||
def automate_sprint_management(repo):
|
||||
"""完整的 Sprint 自动化管理"""
|
||||
|
||||
# Sprint 开始
|
||||
sprint_number = start_new_sprint(repo)
|
||||
|
||||
# Sprint 监控
|
||||
monitor_sprint_progress(repo, sprint_number)
|
||||
|
||||
# Sprint 结束
|
||||
end_sprint(repo, sprint_number)
|
||||
generate_sprint_report(repo, sprint_number)
|
||||
|
||||
# 执行 Sprint 工作流
|
||||
sprint_result = automate_sprint_management(project)
|
||||
```
|
||||
|
||||
## 完整的端到端示例
|
||||
|
||||
### 示例:自动化 Issue 到 Release 流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# 完整的自动化工作流脚本
|
||||
|
||||
OWNER="ai-agent"
|
||||
REPO="demo-project"
|
||||
PROJECT_NAME="AI Agent Demo"
|
||||
|
||||
echo "🤖 启动 AI Agent 自动化工作流..."
|
||||
|
||||
# 阶段 1: 项目创建
|
||||
echo "📦 阶段 1: 创建项目"
|
||||
gitlink-cli repo +create \
|
||||
--name "$REPO" \
|
||||
--description "$PROJECT_NAME" \
|
||||
--private false
|
||||
|
||||
# 初始化本地仓库
|
||||
cd "$REPO"
|
||||
git init
|
||||
git remote add gitlink "https://www.gitlink.org.cn/$OWNER/$REPO.git"
|
||||
|
||||
# 创建基础文件
|
||||
echo "# $PROJECT_NAME" > README.md
|
||||
echo "MIT License" > LICENSE
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git push -u gitlink master:master
|
||||
|
||||
# 设置分支保护
|
||||
gitlink-cli branch +protect --owner "$OWNER" --repo "$REPO" --name master
|
||||
|
||||
# 阶段 2: 创建开发 Issue
|
||||
echo "🎯 阶段 2: 创建开发 Issue"
|
||||
FEATURES=(
|
||||
"用户认证系统"
|
||||
"数据管理模块"
|
||||
"API 接口开发"
|
||||
"前端界面设计"
|
||||
"测试框架搭建"
|
||||
)
|
||||
|
||||
for feature in "${FEATURES[@]}"; do
|
||||
gitlink-cli issue +create \
|
||||
--owner "$OWNER" \
|
||||
--repo "$REPO" \
|
||||
--title "开发 $feature" \
|
||||
--body "## 任务描述
|
||||
|
||||
实现 $feature 功能
|
||||
|
||||
## 技术要求
|
||||
- 代码规范
|
||||
- 单元测试
|
||||
- 文档完整
|
||||
|
||||
## 验收标准
|
||||
- 功能正常工作
|
||||
- 测试通过
|
||||
- 代码审查通过"
|
||||
done
|
||||
|
||||
# 阶段 3: 模拟开发和 PR 创建
|
||||
echo "🔧 阶段 3: 模拟开发工作"
|
||||
|
||||
# 创建功能分支
|
||||
for feature in "${FEATURES[@]}"; do
|
||||
# 模拟分支名(将中文转为拼音)
|
||||
branch_name="feature-$(echo $feature | md5sum | cut -c1-8)"
|
||||
|
||||
git checkout -b "$branch_name"
|
||||
|
||||
# 模拟开发工作
|
||||
echo "// $feature 实现" > "${feature}.js"
|
||||
git add .
|
||||
git commit -m "Implement $feature"
|
||||
git push gitlink "$branch_name"
|
||||
|
||||
# 创建 PR
|
||||
gitlink-cli pr +create \
|
||||
--owner "$OWNER" \
|
||||
--repo "$REPO" \
|
||||
--title "Feature: $feature" \
|
||||
--head "$branch_name" \
|
||||
--base master \
|
||||
--body "## 功能说明
|
||||
|
||||
实现 $feature 功能
|
||||
|
||||
## 变更内容
|
||||
- 添加核心功能
|
||||
- 实现相关测试
|
||||
- 更新文档
|
||||
|
||||
## 测试情况
|
||||
- 单元测试通过
|
||||
- 集成测试通过
|
||||
- 手工测试完成"
|
||||
|
||||
git checkout master
|
||||
done
|
||||
|
||||
# 阶段 4: 自动 Issue 分类
|
||||
echo "🏷️ 阶段 4: 自动分类 Issue"
|
||||
|
||||
# 获取所有开放 Issue
|
||||
ISSUES=$(gitlink-cli issue +list --owner "$OWNER" --repo "$REPO" --state open --format json)
|
||||
|
||||
# 为 Issue 添加标签
|
||||
echo "$ISSUES" | jq -r '.data.issues[].id' | while read issue_id; do
|
||||
echo "处理 Issue #$issue_id"
|
||||
|
||||
# 获取 Issue 详情
|
||||
ISSUE_DETAIL=$(gitlink-cli issue +view --owner "$OWNER" --repo "$REPO" --id "$issue_id" --format json)
|
||||
TITLE=$(echo "$ISSUE_DETAIL" | jq -r '.data.subject')
|
||||
|
||||
# 基于标题分类
|
||||
if echo "$TITLE" | grep -iq "认证"; then
|
||||
echo " → 分类为: feature + security"
|
||||
# 实际执行时取消注释
|
||||
# gitlink-cli api POST "/$OWNER/$REPO/issues/$issue_id" --body '{"issue_tag_ids":[1,5]}'
|
||||
else
|
||||
echo " → 分类为: feature"
|
||||
# gitlink-cli api POST "/$OWNER/$REPO/issues/$issue_id" --body '{"issue_tag_ids":[1]}'
|
||||
fi
|
||||
done
|
||||
|
||||
# 阶段 5: PR 审查
|
||||
echo "🔍 阶段 5: 自动 PR 审查"
|
||||
|
||||
# 获取开放 PR
|
||||
PRS=$(gitlink-cli pr +list --owner "$OWNER" --repo "$REPO" --state open --format json)
|
||||
|
||||
echo "$PRS" | jq -r '.data.prs[].id' | while read pr_id; do
|
||||
echo "审查 PR #$pr_id"
|
||||
|
||||
# 获取 PR 详情
|
||||
PR_DETAIL=$(gitlink-cli pr +view --owner "$OWNER" --repo "$REPO" --id "$pr_id" --format json)
|
||||
PR_AUTHOR=$(echo "$PR_DETAIL" | jq -r '.data.author.login')
|
||||
PR_TITLE=$(echo "$PR_DETAIL" | jq -r '.data.title')
|
||||
|
||||
# 简单的代码检查(这里只是模拟)
|
||||
REVIEW_COMMENTS="# 🔍 自动审查结果
|
||||
|
||||
## PR 信息
|
||||
- **标题**: $PR_TITLE
|
||||
- **作者**: $PR_AUTHOR
|
||||
- **状态**: 待审查
|
||||
|
||||
## ✅ 自动检查
|
||||
- 代码提交正常
|
||||
- 变更描述清晰
|
||||
- 符合项目规范
|
||||
|
||||
## 💡 建议
|
||||
- 添加单元测试
|
||||
- 更新相关文档
|
||||
- 确认向后兼容性
|
||||
|
||||
## 📋 审查结论
|
||||
代码质量良好,建议合并。"
|
||||
|
||||
echo " → 添加审查评论"
|
||||
# 实际执行时取消注释
|
||||
# gitlink-cli api POST "/$OWNER/$REPO/pulls/$pr_id/reviews" --body "{\"body\":\"$REVIEW_COMMENTS\",\"event\":\"APPROVE\"}"
|
||||
done
|
||||
|
||||
# 阶段 6: 生成 Release Notes
|
||||
echo "📝 阶段 6: 生成 Release Notes"
|
||||
|
||||
# 合并所有 PR(模拟)
|
||||
echo "合并所有功能 PR..."
|
||||
MERGED_PRS=$(gitlink-cli pr +list --owner "$OWNER" --repo "$REPO" --state merged --format json)
|
||||
|
||||
# 生成 Release Notes
|
||||
RELEASE_NOTES="# 🎉 v1.0.0 首个版本发布
|
||||
|
||||
## 📊 版本概述
|
||||
这是 $PROJECT_NAME 的首个稳定版本,包含了核心功能的完整实现。
|
||||
|
||||
## ✨ 新功能
|
||||
- 用户认证系统:完整的登录注册功能
|
||||
- 数据管理模块:高效的数据存储和检索
|
||||
- API 接口:RESTful API 设计
|
||||
- 前端界面:现代化的用户界面
|
||||
- 测试框架:完整的自动化测试
|
||||
|
||||
## 🐛 Bug 修复
|
||||
- 修复认证过程中的边界问题
|
||||
- 解决数据一致性问题
|
||||
- 优化 API 响应性能
|
||||
|
||||
## 🔧 技术改进
|
||||
- 代码结构优化
|
||||
- 性能提升 30%
|
||||
- 安全性增强
|
||||
|
||||
## 📚 文档更新
|
||||
- 用户手册完善
|
||||
- API 文档更新
|
||||
- 开发指南补充
|
||||
|
||||
## 🙏 贡献者
|
||||
感谢所有参与开发的贡献者!
|
||||
|
||||
## 📥 安装方法
|
||||
\`\`\`bash
|
||||
# 使用 npm 安装
|
||||
npm install $OWNER/$REPO@v1.0.0
|
||||
|
||||
# 或使用 yarn
|
||||
yarn add $OWNER/$REPO@v1.0.0
|
||||
\`\`\`
|
||||
|
||||
## 🔄 升级指南
|
||||
从之前的版本升级,请参考迁移指南。
|
||||
|
||||
## 📚 完整文档
|
||||
- 用户指南: https://www.gitlink.org.cn/$OWNER/$REPO/wiki
|
||||
- API 文档: https://www.gitlink.org.cn/$OWNER/$REPO/api-docs
|
||||
|
||||
---
|
||||
**发布日期**: $(date +%Y-%m-%d)
|
||||
**下一版本**: v1.1.0 (计划于 $(date -d "1 month" +%Y-%m-%d) 发布)"
|
||||
|
||||
# 创建 Release
|
||||
echo "创建 Release v1.0.0..."
|
||||
gitlink-cli release +create \
|
||||
--owner "$OWNER" \
|
||||
--repo "$REPO" \
|
||||
--tag "v1.0.0" \
|
||||
--name "v1.0.0" \
|
||||
--body "$RELEASE_NOTES"
|
||||
|
||||
# 阶段 7: Sprint 报告
|
||||
echo "📊 阶段 7: 生成 Sprint 报告"
|
||||
|
||||
SPRINT_START=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
SPRINT_END=$(date +%Y-%m-%d)
|
||||
|
||||
SPRINT_REPORT="# 📊 Sprint 1 完成报告
|
||||
|
||||
## 📅 时间信息
|
||||
- **Sprint 周期**: $SPRINT_START 至 $SPRINT_END
|
||||
- **团队规模**: AI Agent x 1
|
||||
- **工作模式**: 自动化开发
|
||||
|
||||
## 🎯 目标达成
|
||||
### 计划完成度
|
||||
- **计划 Issue**: 5 个
|
||||
- **实际完成**: 5 个
|
||||
- **完成率**: 100%
|
||||
|
||||
### 质量指标
|
||||
- **代码质量**: 优秀
|
||||
- **测试覆盖率**: 95%
|
||||
- **文档完整度**: 100%
|
||||
|
||||
## 💻 工作统计
|
||||
### 代码提交
|
||||
- **总提交数**: 42 次
|
||||
- **日均提交**: 3 次/天
|
||||
- **代码行数**: +2,450 -180 行
|
||||
|
||||
### Issue 处理
|
||||
- **关闭 Issue**: 5 个
|
||||
- **新建 Issue**: 0 个
|
||||
- **平均处理时间**: 2.5 天
|
||||
|
||||
### PR 合并
|
||||
- **合并 PR**: 5 个
|
||||
- **平均审查时间**: 1 小时
|
||||
- **平均合并时间**: 2 小时
|
||||
|
||||
## 🎉 主要成就
|
||||
1. ✅ 完成用户认证系统开发
|
||||
2. ✅ 实现数据管理模块
|
||||
3. ✅ 构建 RESTful API
|
||||
4. ✅ 设计现代化前端界面
|
||||
5. ✅ 建立完整测试体系
|
||||
|
||||
## 📈 性能指标
|
||||
- **开发效率**: 高
|
||||
- **代码质量**: 优秀
|
||||
- **自动化程度**: 95%
|
||||
- **文档完整度**: 100%
|
||||
|
||||
## 🔮 下期规划
|
||||
- 性能优化和改进
|
||||
- 新功能模块开发
|
||||
- 国际化支持
|
||||
- 移动端适配
|
||||
|
||||
---
|
||||
**AI Agent 自动化工作流演示**
|
||||
**报告生成**: $(date +%Y-%m-%d %H:%M:%S)"
|
||||
|
||||
# 保存 Sprint 报告
|
||||
REPORT_FILE="sprint_reports/sprint_1_$(date +%Y%m%d).md"
|
||||
mkdir -p sprint_reports
|
||||
echo "$SPRINT_REPORT" > "$REPORT_FILE"
|
||||
|
||||
echo "🎉 AI Agent 自动化工作流完成!"
|
||||
echo ""
|
||||
echo "📊 项目统计:"
|
||||
echo " 仓库: https://www.gitlink.org.cn/$OWNER/$REPO"
|
||||
echo " Issue: 5 个全部完成"
|
||||
echo " PR: 5 个全部合并"
|
||||
echo " Release: v1.0.0 已发布"
|
||||
echo ""
|
||||
echo "📄 生成的文档:"
|
||||
echo " - Release Notes: https://www.gitlink.org.cn/$OWNER/$REPO/releases/v1.0.0"
|
||||
echo " - Sprint 报告: $REPORT_FILE"
|
||||
```
|
||||
|
||||
## Claude Code 集成示例
|
||||
|
||||
### 在 Claude Code 中使用工作流
|
||||
|
||||
```markdown
|
||||
# 用户指令
|
||||
|
||||
帮助我创建一个新的项目并完成首个版本的发布。
|
||||
|
||||
# Claude Code 执行
|
||||
|
||||
我会使用 gitlink-cli 的自动化工作流来完成这个任务:
|
||||
|
||||
1. **创建仓库** - 使用 workflow-repo-setup
|
||||
2. **管理 Issue** - 使用 workflow-issue-triage
|
||||
3. **审查 PR** - 使用 workflow-pr-review
|
||||
4. **生成 Release** - 使用 workflow-release-notes
|
||||
5. **总结报告** - 使用 workflow-sprint-report
|
||||
|
||||
让我开始执行...
|
||||
```
|
||||
|
||||
### 技能组合使用
|
||||
|
||||
```python
|
||||
# AI Agent 多技能组合
|
||||
class GitLinkAgent:
|
||||
def __init__(self, owner, repo):
|
||||
self.owner = owner
|
||||
self.repo = repo
|
||||
self.cli = "gitlink-cli"
|
||||
|
||||
def complete_project_workflow(self):
|
||||
"""完整的项目工作流"""
|
||||
|
||||
# 阶段 1: 初始化
|
||||
self.setup_repository()
|
||||
|
||||
# 阶段 2: 开发管理
|
||||
self.manage_development()
|
||||
|
||||
# 阶段 3: 质量控制
|
||||
self.automated_review()
|
||||
|
||||
# 阶段 4: 发布管理
|
||||
self.create_release()
|
||||
|
||||
# 阶段 5: 报告总结
|
||||
self.generate_reports()
|
||||
|
||||
def setup_repository(self):
|
||||
"""仓库初始化"""
|
||||
# 使用 workflow-repo-setup
|
||||
create_repo_cmd = f"{self.cli} repo +create --name {self.repo}"
|
||||
subprocess.run(create_repo_cmd.split())
|
||||
|
||||
protect_branch_cmd = f"{self.cli} branch +protect --name master"
|
||||
subprocess.run(protect_branch_cmd.split())
|
||||
|
||||
def manage_development(self):
|
||||
"""开发管理"""
|
||||
# 监控新 Issue 并自动分类
|
||||
issues = self.get_new_issues()
|
||||
for issue in issues:
|
||||
self.classify_issue(issue)
|
||||
|
||||
# 监控新 PR 并审查
|
||||
prs = self.get_new_prs()
|
||||
for pr in prs:
|
||||
self.review_pr(pr)
|
||||
|
||||
def automated_review(self):
|
||||
"""自动化审查"""
|
||||
# 获取待审查的 PR
|
||||
pending_prs = self.get_pending_prs()
|
||||
|
||||
for pr in pending_prs:
|
||||
review_result = self.analyze_pr(pr)
|
||||
self.submit_review(pr, review_result)
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 工作流选择
|
||||
- **项目创建**: 使用 workflow-repo-setup
|
||||
- **日常维护**: 使用 workflow-issue-triage 和 workflow-pr-review
|
||||
- **版本发布**: 使用 workflow-release-notes
|
||||
- **团队管理**: 使用 workflow-sprint-report
|
||||
|
||||
### 2. 执行顺序
|
||||
典型的执行顺序:
|
||||
1. 项目初始化 → 2. Issue 管理 → 3. PR 审查 → 4. Release 发布 → 5. Sprint 报告
|
||||
|
||||
### 3. 错误处理
|
||||
```python
|
||||
def safe_workflow_execution(workflow_func, *args, **kwargs):
|
||||
"""安全执行工作流"""
|
||||
try:
|
||||
return workflow_func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# 记录错误
|
||||
log_error(e)
|
||||
# 尝试恢复
|
||||
return handle_workflow_error(e, workflow_func, *args, **kwargs)
|
||||
```
|
||||
|
||||
### 4. 进度跟踪
|
||||
```python
|
||||
class WorkflowProgress:
|
||||
def __init__(self):
|
||||
self.current_step = 0
|
||||
self.total_steps = 5
|
||||
self.completed_steps = []
|
||||
self.failed_steps = []
|
||||
|
||||
def update_progress(self, step_name, success=True):
|
||||
if success:
|
||||
self.completed_steps.append(step_name)
|
||||
else:
|
||||
self.failed_steps.append(step_name)
|
||||
self.current_step += 1
|
||||
|
||||
def get_progress_report(self):
|
||||
progress = self.current_step / self.total_steps * 100
|
||||
return {
|
||||
'progress': f'{progress:.1f}%',
|
||||
'completed': self.completed_steps,
|
||||
'failed': self.failed_steps
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展工作流
|
||||
|
||||
### 自定义工作流
|
||||
|
||||
```python
|
||||
# 创建自定义工作流
|
||||
def custom_workflow(owner, repo, custom_config):
|
||||
"""自定义工作流模板"""
|
||||
|
||||
# 1. 预检查
|
||||
if not validate_environment():
|
||||
return False
|
||||
|
||||
# 2. 执行自定义步骤
|
||||
for step in custom_config['steps']:
|
||||
execute_step(step)
|
||||
|
||||
# 3. 后处理
|
||||
cleanup_environment()
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
|------|----------|
|
||||
| 权限不足 | 检查 Token 权限 |
|
||||
| API 限制 | 添加重试机制 |
|
||||
| 数据格式错误 | 验证输入数据 |
|
||||
| 执行超时 | 增加超时时间 |
|
||||
|
||||
## References
|
||||
|
||||
- [workflow-issue-triage](../references/workflow-issue-triage.md) — Issue 分类
|
||||
- [workflow-pr-review](../references/workflow-pr-review.md) — PR 审查
|
||||
- [workflow-release-notes](../references/workflow-release-notes.md) — Release Notes
|
||||
- [workflow-repo-setup](../references/workflow-repo-setup.md) — 仓库初始化
|
||||
- [workflow-sprint-report](../references/workflow-sprint-report.md) — Sprint 报告
|
||||
- [gitlink-workflow](../SKILL.md) — 工作流总览
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
# Workflow: Issue Triage(Issue 自动分类)
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **AI Agent 工作流**:此工作流专为 AI Agent 设计,用于自动化 Issue 分类和管理。
|
||||
|
||||
AI Agent 自动为新建的 Issue 添加标签和分类,提高项目管理效率。
|
||||
|
||||
## 工作流概述
|
||||
|
||||
Issue Triage 工作流通过分析 Issue 的标题和描述内容,自动为 Issue 分配合适的标签,帮助项目维护者更好地组织和管理 Issue。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- **项目维护**:自动分类新提交的 Issue
|
||||
- **标签管理**:确保 Issue 有正确的分类标签
|
||||
- **优先级排序**:基于分类快速识别高优先级 Issue
|
||||
- **团队协作**:减少手动分类工作量,提高效率
|
||||
|
||||
## 工作流步骤
|
||||
|
||||
### 步骤 1:获取未标记的 Issue 列表
|
||||
|
||||
```bash
|
||||
# 获取所有开放的 Issue
|
||||
gitlink-cli issue +list --state open --format json
|
||||
|
||||
# 筛选出没有标签的 Issue
|
||||
gitlink-cli issue +list --state open --format json | \
|
||||
jq '.data.issues[] | select(.issue_tags == null or .issue_tags == [])'
|
||||
```
|
||||
|
||||
### 步骤 2:分析 Issue 内容
|
||||
|
||||
```bash
|
||||
# 获取特定 Issue 的详细信息
|
||||
gitlink-cli issue +view --id 123 --format json
|
||||
|
||||
# 分析标题和描述
|
||||
gitlink-cli issue +view --id 123 --format json | \
|
||||
jq '{subject: .data.subject, description: .data.description}'
|
||||
```
|
||||
|
||||
### 步骤 3:智能分类
|
||||
|
||||
基于 Issue 内容的分析,应用以下分类规则:
|
||||
|
||||
**Bug 分类规则**:
|
||||
- 标题/描述包含关键词:`bug`、`错误`、`失败`、`异常`、`crash`、`issue`、`problem`
|
||||
- 行为模式:描述功能失效或异常行为
|
||||
- 示例:`登录时遇到错误`、`页面加载失败`
|
||||
|
||||
**Feature 分类规则**:
|
||||
- 标题/描述包含关键词:`feature`、`新增`、`建议`、`request`、`enhancement`、`improve`
|
||||
- 行为模式:建议新功能或改进
|
||||
- 示例:`添加用户权限管理`、`建议支持暗色主题`
|
||||
|
||||
**Question 分类规则**:
|
||||
- 标题/描述包含关键词:`question`、`如何`、`怎么`、`how`、`帮助`、`help`、`疑问`
|
||||
- 行为模式:询问使用方法或寻求帮助
|
||||
- 示例:`如何配置环境变量`、`怎么部署到服务器`
|
||||
|
||||
**Documentation 分类规则**:
|
||||
- 标题/描述包含关键词:`doc`、`文档`、`README`、`tutorial`、`guide`、`example`
|
||||
- 行为模式:与文档相关的问题或建议
|
||||
- 示例:`更新安装文档`、`添加使用示例`
|
||||
|
||||
### 步骤 4:添加标签
|
||||
|
||||
```bash
|
||||
# 获取项目的标签列表
|
||||
gitlink-cli api GET /:owner/:repo/issue_tags --format json
|
||||
|
||||
# 为 Issue 添加标签
|
||||
gitlink-cli api POST /:owner/:repo/issues/123 --body \
|
||||
'{"issue_tag_ids":[1, 2]}'
|
||||
|
||||
# 添加单个标签
|
||||
gitlink-cli api POST /:owner/:repo/issues/123 --body \
|
||||
'{"issue_tag_ids":[1]}'
|
||||
```
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Issue Triage 自动化脚本
|
||||
|
||||
OWNER="myuser"
|
||||
REPO="myproject"
|
||||
|
||||
# 1. 获取所有开放的 Issue
|
||||
ISSUES=$(gitlink-cli issue +list --owner $OWNER --repo $REPO --state open --format json)
|
||||
|
||||
# 2. 遍历每个 Issue
|
||||
echo "$ISSUES" | jq -c '.data.issues[]' | while read -r issue; do
|
||||
ISSUE_ID=$(echo "$issue" | jq -r '.id')
|
||||
SUBJECT=$(echo "$issue" | jq -r '.subject')
|
||||
DESCRIPTION=$(echo "$issue" | jq -r '.description')
|
||||
TAGS=$(echo "$issue" | jq -r '.issue_tags // []')
|
||||
|
||||
# 跳过已有标签的 Issue
|
||||
if [ "$TAGS" != "[]" ]; then
|
||||
echo "Issue $ISSUE_ID 已有标签,跳过"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "分析 Issue $ISSUE_ID: $SUBJECT"
|
||||
|
||||
# 分析内容并确定标签
|
||||
TAG_IDS=()
|
||||
CONTENT="$SUBJECT $DESCRIPTION"
|
||||
|
||||
# 分类逻辑
|
||||
if echo "$CONTENT" | grep -iqE "bug|错误|失败|异常|crash|issue|problem"; then
|
||||
TAG_IDS+=("1") # 假设 1 是 bug 标签
|
||||
echo " → 分类为: bug"
|
||||
fi
|
||||
|
||||
if echo "$CONTENT" | grep -iqE "feature|新增|建议|request|enhancement|improve"; then
|
||||
TAG_IDS+=("2") # 假设 2 是 enhancement 标签
|
||||
echo " → 分类为: enhancement"
|
||||
fi
|
||||
|
||||
if echo "$CONTENT" | grep -iqE "question|如何|怎么|how|帮助|help|疑问"; then
|
||||
TAG_IDS+=("3") # 假设 3 是 question 标签
|
||||
echo " → 分类为: question"
|
||||
fi
|
||||
|
||||
if echo "$CONTENT" | grep -iqE "doc|文档|README|tutorial|guide|example"; then
|
||||
TAG_IDS+=("4") # 假设 4 是 documentation 标签
|
||||
echo " → 分类为: documentation"
|
||||
fi
|
||||
|
||||
# 添加标签到 Issue
|
||||
if [ ${#TAG_IDS[@]} -gt 0 ]; then
|
||||
echo " → 为 Issue $ISSUE_ID 添加标签: ${TAG_IDS[*]}"
|
||||
# 实际执行时取消注释
|
||||
# gitlink-cli api POST "/$OWNER/$REPO/issues/$ISSUE_ID" --body \
|
||||
# "{\"issue_tag_ids\":[${TAG_IDS[*]}]}"
|
||||
else
|
||||
echo " → 无法自动分类,需要人工处理"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
```
|
||||
|
||||
## AI Agent 集成示例
|
||||
|
||||
Claude Code 等 AI Agent 可以直接执行此工作流:
|
||||
|
||||
```python
|
||||
# AI Agent 执行 Issue Triage
|
||||
def issue_triage(owner, repo):
|
||||
"""AI Agent 自动分类 Issue"""
|
||||
|
||||
# 1. 获取开放的 Issue
|
||||
issues = gitlink_cli_issue_list(owner, repo, state="open")
|
||||
|
||||
for issue in issues:
|
||||
# 2. 跳过已有标签的
|
||||
if issue.get('issue_tags'):
|
||||
continue
|
||||
|
||||
# 3. AI 分析内容
|
||||
content = f"{issue['subject']} {issue.get('description', '')}"
|
||||
classification = analyze_issue_content(content)
|
||||
|
||||
# 4. 添加标签
|
||||
if classification:
|
||||
add_issue_tags(owner, repo, issue['id'], classification)
|
||||
|
||||
def analyze_issue_content(content):
|
||||
"""AI 分析 Issue 内容"""
|
||||
# 使用 AI 模型分析文本
|
||||
labels = []
|
||||
|
||||
if any(word in content.lower() for word in ['bug', 'error', 'fail']):
|
||||
labels.append('bug')
|
||||
|
||||
if any(word in content.lower() for word in ['feature', 'enhancement']):
|
||||
labels.append('enhancement')
|
||||
|
||||
return labels
|
||||
```
|
||||
|
||||
## 高级分类策略
|
||||
|
||||
### 多标签分类
|
||||
一个 Issue 可以有多个标签:
|
||||
|
||||
```bash
|
||||
# 同时添加多个标签
|
||||
gitlink-cli api POST /:owner/:repo/issues/123 --body \
|
||||
'{"issue_tag_ids":[1, 2, 5]}'
|
||||
|
||||
# 示例:既严重又是功能请求
|
||||
# bug + enhancement + high-priority
|
||||
```
|
||||
|
||||
### 优先级分类
|
||||
基于紧急程度添加优先级标签:
|
||||
|
||||
**高优先级关键词**:
|
||||
- `urgent`、`紧急`、`严重`、`critical`、`blocking`、`阻塞`
|
||||
|
||||
**中优先级关键词**:
|
||||
- `moderate`、`中等`、`normal`、`常规`
|
||||
|
||||
**低优先级关键词**:
|
||||
- `low`、`较低`、`minor`、`次要`、`nice-to-have`
|
||||
|
||||
### 复杂度分类
|
||||
基于实现难度分类:
|
||||
|
||||
**简单**:
|
||||
- 关键词:`简单`、`easy`、`quick`、`minor`
|
||||
- 预估时间:1-2 天
|
||||
|
||||
**中等**:
|
||||
- 关键词:`中等`、`moderate`、`normal`
|
||||
- 预估时间:3-7 天
|
||||
|
||||
**复杂**:
|
||||
- 关键词:`复杂`、`complex`、`hard`、`major`、`重构`
|
||||
- 预估时间:8+ 天
|
||||
|
||||
## 自定义分类规则
|
||||
|
||||
根据项目特点定制分类规则:
|
||||
|
||||
```bash
|
||||
# Web 项目特定分类
|
||||
WEB_KEYWORDS=("前端" "frontend" "UI" "界面" "页面")
|
||||
if grep -qE "${WEB_KEYWORDS[*]}" <<< "$CONTENT"; then
|
||||
TAG_IDS+=("10") # frontend 标签
|
||||
fi
|
||||
|
||||
# 后端项目特定分类
|
||||
BACKEND_KEYWORDS=("后端" "backend" "API" "接口" "数据库")
|
||||
if grep -qE "${BACKEND_KEYWORDS[*]}" <<< "$CONTENT"; then
|
||||
TAG_IDS+=("11") # backend 标签
|
||||
fi
|
||||
|
||||
# DevOps 相关分类
|
||||
DEVOPS_KEYWORDS=("部署" "deploy" "CI" "CD" "Docker" "Kubernetes")
|
||||
if grep -qE "${DEVOPS_KEYWORDS[*]}" <<< "$CONTENT"; then
|
||||
TAG_IDS+=("12") # devops 标签
|
||||
fi
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
常见问题处理:
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| 标签 ID 不存在 | 标签未创建 | 先创建项目标签 |
|
||||
| 权限不足 | 无修改 Issue 权限 | 联系项目管理员 |
|
||||
| 分类不准确 | 关键词匹配失败 | 优化分类规则或人工审核 |
|
||||
|
||||
## 质量保证
|
||||
|
||||
确保分类质量的措施:
|
||||
|
||||
1. **定期审查**:定期审查自动分类结果
|
||||
2. **反馈学习**:根据反馈调整分类规则
|
||||
3. **人工确认**:对不确定的分类进行人工确认
|
||||
4. **规则优化**:持续优化关键词匹配规则
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **渐进式部署**:先小范围测试,再全面应用
|
||||
2. **规则透明**:记录分类规则,便于团队理解和调整
|
||||
3. **性能监控**:监控分类准确率和效率
|
||||
4. **用户反馈**:收集用户反馈,持续改进
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 自动分配
|
||||
基于分类自动分配给合适的开发者:
|
||||
|
||||
```bash
|
||||
# Bug 分配给核心开发者
|
||||
if [[ " ${TAG_IDS[@]} " =~ " 1 " ]]; then
|
||||
ASSIGNEE="senior_developer"
|
||||
fi
|
||||
|
||||
# 文档问题分配给技术写作
|
||||
if [[ " ${TAG_IDS[@]} " =~ " 4 " ]]; then
|
||||
ASSIGNEE="tech_writer"
|
||||
fi
|
||||
```
|
||||
|
||||
### 自动设置优先级
|
||||
基于分类和关键词自动设置优先级:
|
||||
|
||||
```bash
|
||||
# 严重 bug 设置为高优先级
|
||||
if [[ " ${TAG_IDS[@]} " =~ " 1 " ]] && echo "$CONTENT" | grep -iq "严重"; then
|
||||
PRIORITY_ID="1" # 高优先级
|
||||
fi
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [workflow-pr-review](workflow-pr-review.md) — PR 审查工作流
|
||||
- [workflow-release-notes](workflow-release-notes.md) — Release Notes 生成
|
||||
- [gitlink-workflow](../SKILL.md) — 工作流总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [issue +list](../../gitlink-issue/references/gitlink-issue-list.md) — Issue 列表
|
||||
- [issue +view](../../gitlink-issue/references/gitlink-issue-view.md) — 查看 Issue
|
||||
|
|
@ -0,0 +1,397 @@
|
|||
# Workflow: PR Review(代码审查辅助)
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **AI Agent 工作流**:此工作流专为 AI Agent 设计,用于辅助代码审查。
|
||||
|
||||
AI Agent 获取 PR 变更内容,分析代码质量,自动添加 Review 评论,提高代码审查效率。
|
||||
|
||||
## 工作流概述
|
||||
|
||||
PR Review 工作流通过分析 Pull Request 的代码变更,自动识别潜在问题、提出改进建议,并生成结构化的审查意见。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- **代码审查**:自动化 PR 初步审查
|
||||
- **质量检查**:检查代码质量和规范合规性
|
||||
- **安全审查**:识别潜在的安全问题
|
||||
- **性能分析**:评估性能相关代码变更
|
||||
- **文档检查**:验证代码注释和文档完整性
|
||||
|
||||
## 工作流步骤
|
||||
|
||||
### 步骤 1:获取 PR 详情
|
||||
|
||||
```bash
|
||||
# 获取 PR 基本信息
|
||||
gitlink-cli pr +view --id 42 --format json
|
||||
|
||||
# 提取关键信息
|
||||
PR_INFO=$(gitlink-cli pr +view --id 42 --format json | jq '.data')
|
||||
PR_TITLE=$(echo "$PR_INFO" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_INFO" | jq -r '.author.login')
|
||||
SOURCE_BRANCH=$(echo "$PR_INFO" | jq -r '.head_ref')
|
||||
TARGET_BRANCH=$(echo "$PR_INFO" | jq -r '.base_ref')
|
||||
```
|
||||
|
||||
### 步骤 2:获取变更文件列表
|
||||
|
||||
```bash
|
||||
# 获取 PR 变更的文件列表
|
||||
gitlink-cli pr +files --id 42 --format json
|
||||
|
||||
# 分析文件变更
|
||||
FILES_CHANGED=$(gitlink-cli pr +files --id 42 --format json | \
|
||||
jq '.data.files[] |
|
||||
{filename: .filename,
|
||||
status: .status,
|
||||
additions: .additions,
|
||||
deletions: .deletions}')
|
||||
```
|
||||
|
||||
### 步骤 3:获取代码差异
|
||||
|
||||
```bash
|
||||
# 获取 PR 的完整代码差异
|
||||
gitlink-cli pr +diff --id 42 --format json
|
||||
|
||||
# 提取特定文件的差异
|
||||
gitlink-cli pr +diff --id 42 --format json | \
|
||||
jq '.data.diff | split("diff --git")'
|
||||
```
|
||||
|
||||
### 步骤 4:代码质量分析
|
||||
|
||||
分析代码变更的多个维度:
|
||||
|
||||
**安全性分析**:
|
||||
```bash
|
||||
# 检查敏感信息泄露
|
||||
if echo "$DIFF" | grep -iE "password|secret|api_key|token"; then
|
||||
SECURITY_ISSUES+=("可能包含敏感信息")
|
||||
fi
|
||||
|
||||
# 检查 SQL 注入风险
|
||||
if echo "$DIFF" | grep -iE "SELECT.*FROM.*WHERE.*\$"; then
|
||||
SECURITY_ISSUES+=("可能的 SQL 注入风险")
|
||||
fi
|
||||
```
|
||||
|
||||
**代码规范检查**:
|
||||
```bash
|
||||
# 检查代码风格
|
||||
if echo "$DIFF" | grep -P "\t"; then
|
||||
STYLE_ISSUES+=("包含 Tab 字符,建议使用空格")
|
||||
fi
|
||||
|
||||
# 检查长行
|
||||
if echo "$DIFF" | grep ".\{120,\}"; then
|
||||
STYLE_ISSUES+=("包含超过 120 字符的长行")
|
||||
fi
|
||||
```
|
||||
|
||||
**性能分析**:
|
||||
```bash
|
||||
# 检查可能的性能问题
|
||||
if echo "$DIFF" | grep -iE "N\+1|SELECT.*\*|foreach.*query"; then
|
||||
PERF_ISSUES+=("可能的 N+1 查询问题")
|
||||
fi
|
||||
```
|
||||
|
||||
### 步骤 5:生成 Review 评论
|
||||
|
||||
```bash
|
||||
# 生成结构化的 Review 评论
|
||||
REVIEW_BODY="# 🔍 代码审查结果
|
||||
|
||||
## ✅ 优点
|
||||
- 代码结构清晰
|
||||
- 逻辑正确
|
||||
- 遵循项目规范
|
||||
|
||||
## ⚠️ 需要改进
|
||||
${STYLE_ISSUES[@]+($(printf -- "- %s\n" "${STYLE_ISSUES[@]}"))}
|
||||
|
||||
## 🔒 安全问题
|
||||
${SECURITY_ISSUES[@]+($(printf -- "- %s\n" "${SECURITY_ISSUES[@]}"))}
|
||||
|
||||
## 🚀 性能建议
|
||||
${PERF_ISSUES[@]+($(printf -- "- %s\n" "${PERF_ISSUES[@]}"))}
|
||||
|
||||
## 📝 总体评价
|
||||
代码整体质量良好,建议修改上述问题后合并。"
|
||||
|
||||
# 添加 Review 评论
|
||||
gitlink-cli api POST /:owner/:repo/pulls/42/reviews --body \
|
||||
"{\"body\":\"$REVIEW_BODY\",\"event\":\"COMMENT\"}"
|
||||
```
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# PR Review 自动化脚本
|
||||
|
||||
PR_ID=$1
|
||||
OWNER="myuser"
|
||||
REPO="myproject"
|
||||
|
||||
echo "开始审查 PR #$PR_ID..."
|
||||
|
||||
# 1. 获取 PR 详情
|
||||
PR_INFO=$(gitlink-cli pr +view --owner $OWNER --repo $REPO --id $PR_ID --format json)
|
||||
PR_TITLE=$(echo "$PR_INFO" | jq -r '.data.title')
|
||||
PR_AUTHOR=$(echo "$PR_INFO" | jq -r '.data.author.login')
|
||||
ADDITIONS=$(echo "$PR_INFO" | jq -r '.data.additions')
|
||||
DELETIONS=$(echo "$PR_INFO" | jq -r '.data.deletions')
|
||||
|
||||
echo "PR 标题: $PR_TITLE"
|
||||
echo "PR 作者: $PR_AUTHOR"
|
||||
echo "代码变更: +$ADDITIONS -$DELETIONS"
|
||||
|
||||
# 2. 获取变更文件
|
||||
FILES=$(gitlink-cli pr +files --owner $OWNER --repo $REPO --id $PR_ID --format json)
|
||||
|
||||
# 3. 获取代码差异
|
||||
DIFF=$(gitlink-cli pr +diff --owner $OWNER --repo $REPO --id $PR_ID --format json | \
|
||||
jq -r '.data.diff')
|
||||
|
||||
# 4. 分析代码
|
||||
ISSUES=()
|
||||
SUGGESTIONS=()
|
||||
|
||||
# 安全性检查
|
||||
if echo "$DIFF" | grep -iE "password|secret|api_key|token.*="; then
|
||||
ISSUES+=("🔒 安全:可能包含硬编码的敏感信息")
|
||||
fi
|
||||
|
||||
# 代码规范检查
|
||||
if echo "$DIFF" | grep -P "\t"; then
|
||||
SUGGESTIONS+=("📝 规范:建议使用空格代替 Tab")
|
||||
fi
|
||||
|
||||
# 性能检查
|
||||
if echo "$DIFF" | grep -iE "SELECT.*\*.*FROM"; then
|
||||
SUGGESTIONS+=("🚀 性能:建议明确指定字段而不是使用 *")
|
||||
fi
|
||||
|
||||
# 5. 生成 Review 评论
|
||||
if [ ${#ISSUES[@]} -eq 0 ] && [ ${#SUGGESTIONS[@]} -eq 0 ]; then
|
||||
REVIEW_BODY="# ✅ 审查通过
|
||||
|
||||
代码质量良好,没有发现明显问题。可以合并。"
|
||||
EVENT="APPROVE"
|
||||
else
|
||||
REVIEW_BODY="# 🔍 代码审查结果
|
||||
|
||||
## PR 信息
|
||||
- **标题**: $PR_TITLE
|
||||
- **作者**: $PR_AUTHOR
|
||||
- **变更**: +$ADDITIONS -$DELETIONS 行
|
||||
|
||||
## ❌ 需要修复
|
||||
$(printf -- "- %s\n" "${ISSUES[@]}")
|
||||
|
||||
## 💡 改进建议
|
||||
$(printf -- "- %s\n" "${SUGGESTIONS[@]}")
|
||||
|
||||
## 📋 后续步骤
|
||||
1. 修复上述问题
|
||||
2. 确保所有测试通过
|
||||
3. 更新相关文档"
|
||||
EVENT="REQUEST_CHANGES"
|
||||
fi
|
||||
|
||||
# 6. 提交 Review
|
||||
echo "提交 Review 评论..."
|
||||
gitlink-cli api POST "/$OWNER/$REPO/pulls/$PR_ID/reviews" --body \
|
||||
"{\"body\":\"$REVIEW_BODY\",\"event\":\"$EVENT\"}"
|
||||
|
||||
echo "审查完成!"
|
||||
```
|
||||
|
||||
## AI Agent 集成示例
|
||||
|
||||
Claude Code 等 AI Agent 可以深度集成此工作流:
|
||||
|
||||
```python
|
||||
# AI Agent 执行 PR Review
|
||||
def pr_review(owner, repo, pr_id):
|
||||
"""AI Agent 自动代码审查"""
|
||||
|
||||
# 1. 获取 PR 信息
|
||||
pr_info = get_pr_details(owner, repo, pr_id)
|
||||
files_changed = get_pr_files(owner, repo, pr_id)
|
||||
diff_content = get_pr_diff(owner, repo, pr_id)
|
||||
|
||||
# 2. AI 分析代码
|
||||
review_results = {
|
||||
'security': analyze_security(diff_content),
|
||||
'performance': analyze_performance(diff_content),
|
||||
'style': analyze_code_style(diff_content),
|
||||
'documentation': analyze_documentation(files_changed),
|
||||
'testing': analyze_test_coverage(files_changed)
|
||||
}
|
||||
|
||||
# 3. 生成审查意见
|
||||
review_comment = generate_review_comment(pr_info, review_results)
|
||||
|
||||
# 4. 提交 Review
|
||||
submit_review(owner, repo, pr_id, review_comment, review_results)
|
||||
|
||||
def analyze_security(diff_content):
|
||||
"""AI 安全性分析"""
|
||||
issues = []
|
||||
|
||||
# 检查常见安全问题
|
||||
security_patterns = {
|
||||
'hardcoded_secrets': r'password\s*=\s*["\'].*["\']',
|
||||
'sql_injection': r'SELECT.*FROM.*WHERE.*\${',
|
||||
'xss_risk': r'innerHTML\s*=',
|
||||
'command_injection': r'system\(|exec\(.*\$'
|
||||
}
|
||||
|
||||
for issue_name, pattern in security_patterns.items():
|
||||
if re.search(pattern, diff_content, re.IGNORECASE):
|
||||
issues.append({
|
||||
'type': 'security',
|
||||
'severity': 'high',
|
||||
'issue': issue_name,
|
||||
'description': f'检测到 {issue_name} 风险'
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
def generate_review_comment(pr_info, review_results):
|
||||
"""AI 生成结构化审查意见"""
|
||||
comment = f"""# 🔍 AI 代码审查报告
|
||||
|
||||
## PR 概览
|
||||
- **标题**: {pr_info['title']}
|
||||
- **作者**: {pr_info['author']}
|
||||
- **变更**: +{pr_info['additions']} -{pr_info['deletions']} 行
|
||||
- **文件数**: {len(pr_info['files'])}
|
||||
|
||||
## 🔒 安全审查
|
||||
"""
|
||||
|
||||
if review_results['security']:
|
||||
for issue in review_results['security']:
|
||||
comment += f"- ❌ **{issue['issue']}**: {issue['description']}\n"
|
||||
else:
|
||||
comment += "✅ 未发现安全问题\n"
|
||||
|
||||
comment += "\n## 🚀 性能审查\n"
|
||||
# 类似地添加其他审查结果...
|
||||
|
||||
return comment
|
||||
```
|
||||
|
||||
## 审查维度
|
||||
|
||||
### 1. 安全性审查
|
||||
- **敏感信息泄露**:检查硬编码的密码、API 密钥
|
||||
- **注入攻击**:SQL 注入、命令注入、XSS 风险
|
||||
- **权限控制**:检查权限验证逻辑
|
||||
- **数据验证**:输入验证和输出编码
|
||||
|
||||
### 2. 性能审查
|
||||
- **数据库查询**:N+1 查询、缺少索引
|
||||
- **内存使用**:内存泄漏、大对象处理
|
||||
- **算法复杂度**:时间复杂度和空间复杂度
|
||||
- **缓存策略**:缓存命中率和使用合理性
|
||||
|
||||
### 3. 代码质量审查
|
||||
- **代码规范**:命名规范、格式风格
|
||||
- **代码结构**:模块化、可读性、可维护性
|
||||
- **错误处理**:异常处理完整性
|
||||
- **注释文档**:代码注释和文档质量
|
||||
|
||||
### 4. 测试审查
|
||||
- **测试覆盖**:单元测试和集成测试
|
||||
- **测试质量**:测试用例的有效性
|
||||
- **边界条件**:边界值和异常情况测试
|
||||
|
||||
### 5. 文档审查
|
||||
- **API 文档**:接口文档完整性
|
||||
- **用户文档**:用户指南更新
|
||||
- **变更日志**:CHANGELOG 更新
|
||||
|
||||
## 审查决策
|
||||
|
||||
基于分析结果做出审查决策:
|
||||
|
||||
**APPROVE(通过)**:
|
||||
- 无严重问题
|
||||
- 建议性问题可接受
|
||||
- 测试覆盖充分
|
||||
|
||||
**REQUEST_CHANGES(请求修改)**:
|
||||
- 存在严重安全问题
|
||||
- 重要功能缺失
|
||||
- 测试覆盖不足
|
||||
|
||||
**COMMENT(评论)**:
|
||||
- 一般性建议
|
||||
- 文档改进
|
||||
- 代码优化建议
|
||||
|
||||
## 自动化规则
|
||||
|
||||
常见问题的自动检测规则:
|
||||
|
||||
```python
|
||||
AUTO_REVIEW_RULES = {
|
||||
'security': {
|
||||
'hardcoded_password': {
|
||||
'pattern': r'password\s*=\s*["\'][^"\']{8,}["\']',
|
||||
'severity': 'high',
|
||||
'message': '检测到硬编码密码,请使用环境变量或配置文件'
|
||||
},
|
||||
'sql_injection': {
|
||||
'pattern': r'SELECT.*FROM.*WHERE.*\$[a-z_]+',
|
||||
'severity': 'high',
|
||||
'message': '可能的 SQL 注入风险,请使用参数化查询'
|
||||
}
|
||||
},
|
||||
'performance': {
|
||||
'n_plus_one': {
|
||||
'pattern': r'for\s+\$.*\{\s*.*SELECT',
|
||||
'severity': 'medium',
|
||||
'message': '可能的 N+1 查询问题,考虑使用预加载'
|
||||
},
|
||||
'missing_index': {
|
||||
'pattern': r'WHERE.*LIKE.*%.*%',
|
||||
'severity': 'low',
|
||||
'message': '前缀模糊搜索可能无法使用索引'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **逐步审查**:先检查严重问题,再检查一般问题
|
||||
2. **建设性反馈**:提供具体的改进建议
|
||||
3. **平衡严格**:平衡代码质量和开发效率
|
||||
4. **学习改进**:从审查中学习,提高代码质量
|
||||
5. **团队协作**:与开发者沟通,达成共识
|
||||
|
||||
## 质量保证
|
||||
|
||||
确保审查质量:
|
||||
|
||||
1. **双重检查**:重要 PR 进行二次审查
|
||||
2. **审查标准**:建立统一的审查标准
|
||||
3. **审查培训**:培训审查人员
|
||||
4. **反馈收集**:收集对审查质量的反馈
|
||||
5. **持续改进**:优化审查流程和规则
|
||||
|
||||
## References
|
||||
|
||||
- [workflow-issue-triage](workflow-issue-triage.md) — Issue 分类工作流
|
||||
- [workflow-release-notes](workflow-release-notes.md) — Release Notes 生成
|
||||
- [gitlink-workflow](../SKILL.md) — 工作流总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [pr +view](../../gitlink-pr/references/gitlink-pr-view.md) — 查看 PR
|
||||
- [pr +files](../../gitlink-pr/references/gitlink-pr-files.md) — 查看 PR 文件变更
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
# Workflow: Release Notes 生成
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **AI Agent 工作流**:此工作流专为 AI Agent 设计,用于自动生成版本发布说明。
|
||||
|
||||
AI Agent 从提交历史、Issue 和 PR 数据自动生成结构化的 Release Notes,确保发布文档的完整性和准确性。
|
||||
|
||||
## 工作流概述
|
||||
|
||||
Release Notes 生成工作流自动收集版本间的所有变更信息,整理成结构化的发布说明,包含新功能、Bug 修复、破坏性变更等重要信息。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- **版本发布**:为每个新版本生成发布说明
|
||||
- **变更追踪**:追踪版本间的具体变更
|
||||
- **用户沟通**:向用户清晰传达版本更新内容
|
||||
- **历史记录**:维护项目变更历史
|
||||
|
||||
## 工作流步骤
|
||||
|
||||
### 步骤 1:确定版本范围
|
||||
|
||||
```bash
|
||||
# 获取最新标签
|
||||
LATEST_TAG=$(gitlink-cli release +list --format json | \
|
||||
jq -r '.data.releases[0].tag_name')
|
||||
|
||||
# 确定新版本号
|
||||
NEW_TAG="v1.2.0"
|
||||
|
||||
# 或者获取两个标签之间的差异
|
||||
BASE_TAG="v1.1.0"
|
||||
HEAD_TAG="v1.2.0"
|
||||
```
|
||||
|
||||
### 步骤 2:获取提交历史
|
||||
|
||||
```bash
|
||||
# 获取版本间的提交比较
|
||||
gitlink-cli api GET /:owner/:repo/compare/$BASE_TAG...$HEAD_TAG --format json
|
||||
|
||||
# 提取提交信息
|
||||
COMMITS=$(gitlink-cli api GET /:owner/:repo/compare/$BASE_TAG...$HEAD_TAG --format json | \
|
||||
jq '.data.commits[] |
|
||||
{message: .commit.message,
|
||||
author: .commit.author.name,
|
||||
date: .commit.author.date,
|
||||
sha: .sha}')
|
||||
```
|
||||
|
||||
### 步骤 3:获取已关闭的 Issue
|
||||
|
||||
```bash
|
||||
# 获取已关闭的 Issue
|
||||
CLOSED_ISSUES=$(gitlink-cli issue +list --state closed --format json | \
|
||||
jq '.data.issues[] |
|
||||
select(.closed_at >= "'$START_DATE'") |
|
||||
{id: .id,
|
||||
subject: .subject,
|
||||
labels: [.issue_tags[].name],
|
||||
closed_at: .closed_at}')
|
||||
```
|
||||
|
||||
### 步骤 4:获取合并的 PR
|
||||
|
||||
```bash
|
||||
# 获取已合并的 PR
|
||||
MERGED_PRS=$(gitlink-cli pr +list --state merged --format json | \
|
||||
jq '.data.prs[] |
|
||||
select(.merged_at >= "'$START_DATE'") |
|
||||
{id: .id,
|
||||
title: .title,
|
||||
number: .number,
|
||||
author: .author.login,
|
||||
merged_at: .merged_at}')
|
||||
```
|
||||
|
||||
### 步骤 5:分类和整理变更
|
||||
|
||||
```bash
|
||||
# 按变更类型分类
|
||||
FEATURES=()
|
||||
BUG_FIXES=()
|
||||
ENHANCEMENTS=()
|
||||
BREAKING_CHANGES=()
|
||||
|
||||
# 分析 Issue 标签分类
|
||||
while read -r issue; do
|
||||
SUBJECT=$(echo "$issue" | jq -r '.subject')
|
||||
LABELS=$(echo "$issue" | jq -r '.labels[]')
|
||||
|
||||
if echo "$LABELS" | grep -q "feature"; then
|
||||
FEATURES+=("$SUBJECT")
|
||||
elif echo "$LABELS" | grep -q "bug"; then
|
||||
BUG_FIXES+=("$SUBJECT")
|
||||
elif echo "$LABELS" | grep -q "enhancement"; then
|
||||
ENHANCEMENTS+=("$SUBJECT")
|
||||
fi
|
||||
done <<< "$CLOSED_ISSUES"
|
||||
|
||||
# 分析提交信息
|
||||
while read -r commit; do
|
||||
MESSAGE=$(echo "$commit" | jq -r '.message')
|
||||
|
||||
if echo "$MESSAGE" | grep -iq "BREAKING"; then
|
||||
BREAKING_CHANGES+=("$MESSAGE")
|
||||
fi
|
||||
done <<< "$COMMITS"
|
||||
```
|
||||
|
||||
### 步骤 6:生成 Release Notes
|
||||
|
||||
```bash
|
||||
# 生成结构化的 Release Notes
|
||||
RELEASE_NOTES="# 🚀 Release Notes for $NEW_TAG
|
||||
|
||||
## 📝 What's Changed
|
||||
|
||||
### ✨ New Features
|
||||
$(for feature in "${FEATURES[@]}"; do
|
||||
echo "- $feature"
|
||||
done)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
$(for fix in "${BUG_FIXES[@]}"; do
|
||||
echo "- $fix"
|
||||
done)
|
||||
|
||||
### 🔧 Enhancements
|
||||
$(for enhancement in "${ENHANCEMENTS[@]}"; do
|
||||
echo "- $enhancement"
|
||||
done)
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
$(for breaking in "${BREAKING_CHANGES[@]}"; do
|
||||
echo "- $breaking"
|
||||
done)"
|
||||
|
||||
# 创建 Release
|
||||
gitlink-cli release +create --tag $NEW_TAG --name "$NEW_TAG" --body "$RELEASE_NOTES"
|
||||
```
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Release Notes 自动生成脚本
|
||||
|
||||
OWNER="myuser"
|
||||
REPO="myproject"
|
||||
NEW_VERSION=$1
|
||||
|
||||
if [ -z "$NEW_VERSION" ]; then
|
||||
echo "使用方法: $0 <version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "为版本 $NEW_VERSION 生成 Release Notes..."
|
||||
|
||||
# 1. 获取上一个版本
|
||||
PREV_VERSION=$(gitlink-cli release +list --owner $OWNER --repo $REPO --format json | \
|
||||
jq -r '.data.releases[0].tag_name')
|
||||
|
||||
echo "上一个版本: $PREV_VERSION"
|
||||
echo "新版本: $NEW_VERSION"
|
||||
|
||||
# 2. 获取提交比较
|
||||
COMPARE_DATA=$(gitlink-cli api GET "/$OWNER/$REPO/compare/$PREV_VERSION...$NEW_VERSION" --format json)
|
||||
COMMITS=$(echo "$COMPARE_DATA" | jq -r '.data.commits')
|
||||
|
||||
# 3. 获取已关闭的 Issue
|
||||
ISSUES=$(gitlink-cli issue +list --owner $OWNER --repo $REPO --state closed --format json | \
|
||||
jq '.data.issues[]')
|
||||
|
||||
# 4. 获取已合并的 PR
|
||||
PRS=$(gitlink-cli pr +list --owner $OWNER --repo $REPO --state merged --format json | \
|
||||
jq '.data.prs[]')
|
||||
|
||||
# 5. 分析变更数据
|
||||
FEATURE_COUNT=0
|
||||
BUG_FIX_COUNT=0
|
||||
ENHANCEMENT_COUNT=0
|
||||
BREAKING_COUNT=0
|
||||
|
||||
# 分析 Issue
|
||||
FEATURES=$(echo "$ISSUES" | jq -r 'select(.issue_tags[]?.name == "feature") | "- \(.subject) (#\(.id))"')
|
||||
BUG_FIXES=$(echo "$ISSUES" | jq -r 'select(.issue_tags[]?.name == "bug") | "- \(.subject) (#\(.id))"')
|
||||
ENHANCEMENTS=$(echo "$ISSUES" | jq -r 'select(.issue_tags[]?.name == "enhancement") | "- \(.subject) (#\(.id))"')
|
||||
|
||||
# 分析提交中的破坏性变更
|
||||
BREAKING_CHANGES=$(echo "$COMMITS" | jq -r 'select(.commit.message | contains("BREAKING")) | "- \(.commit.message | split("\n")[0])"')
|
||||
|
||||
# 统计数量
|
||||
FEATURE_COUNT=$(echo "$FEATURES" | grep -c "^-" || echo "0")
|
||||
BUG_FIX_COUNT=$(echo "$BUG_FIXES" | grep -c "^-" || echo "0")
|
||||
ENHANCEMENT_COUNT=$(echo "$ENHANCEMENTS" | grep -c "^-" || echo "0")
|
||||
BREAKING_COUNT=$(echo "$BREAKING_CHANGES" | grep -c "^-" || echo "0")
|
||||
|
||||
# 6. 生成 Release Notes
|
||||
RELEASE_NOTES="# 🎉 Release $NEW_VERSION
|
||||
|
||||
## 📊 变更统计
|
||||
- **新功能**: $FEATURE_COUNT 个
|
||||
- **Bug 修复**: $BUG_FIX_COUNT 个
|
||||
- **功能改进**: $ENHANCEMENT_COUNT 个
|
||||
- **破坏性变更**: $BREAKING_COUNT 个
|
||||
|
||||
## ✨ 新功能
|
||||
$FEATURES
|
||||
|
||||
## 🐛 Bug 修复
|
||||
$BUG_FIXES
|
||||
|
||||
## 🔧 功能改进
|
||||
$ENHANCEMENTS
|
||||
|
||||
## ⚠️ 破坏性变更
|
||||
$BREAKING_CHANGES
|
||||
|
||||
## 🙏 贡献者
|
||||
感谢所有参与此版本开发的贡献者!
|
||||
|
||||
## 📥 安装
|
||||
\`\`\`bash
|
||||
npm install $OWNER/$REPO@$NEW_VERSION
|
||||
\`\`\`
|
||||
|
||||
## 📚 文档
|
||||
完整文档请查看: https://www.gitlink.org.cn/$OWNER/$REPO/wiki
|
||||
|
||||
---
|
||||
**完整变更日志**: https://www.gitlink.org.cn/$OWNER/$REPO/compare/$PREV_VERSION...$NEW_VERSION"
|
||||
|
||||
# 7. 创建 Release
|
||||
echo "创建 Release $NEW_VERSION..."
|
||||
gitlink-cli release +create --owner $OWNER --repo $REPO \
|
||||
--tag $NEW_VERSION --name "$NEW_VERSION" --body "$RELEASE_NOTES"
|
||||
|
||||
echo "Release $NEW_VERSION 创建完成!"
|
||||
```
|
||||
|
||||
## AI Agent 集成示例
|
||||
|
||||
Claude Code 等 AI Agent 可以深度集成此工作流:
|
||||
|
||||
```python
|
||||
# AI Agent 生成 Release Notes
|
||||
def generate_release_notes(owner, repo, new_version):
|
||||
"""AI Agent 自动生成发布说明"""
|
||||
|
||||
# 1. 获取版本信息
|
||||
prev_version = get_latest_release(owner, repo)
|
||||
commits = compare_revisions(owner, repo, prev_version, new_version)
|
||||
issues = get_closed_issues(owner, repo, since=prev_version)
|
||||
prs = get_merged_prs(owner, repo, since=prev_version)
|
||||
|
||||
# 2. AI 分析变更
|
||||
changes = analyze_changes(commits, issues, prs)
|
||||
|
||||
# 3. 生成发布说明
|
||||
release_notes = format_release_notes(new_version, changes, prev_version)
|
||||
|
||||
# 4. 创建 Release
|
||||
create_release(owner, repo, new_version, release_notes)
|
||||
|
||||
return release_notes
|
||||
|
||||
def analyze_changes(commits, issues, prs):
|
||||
"""AI 智能分析变更内容"""
|
||||
|
||||
changes = {
|
||||
'features': [],
|
||||
'bug_fixes': [],
|
||||
'enhancements': [],
|
||||
'breaking_changes': [],
|
||||
'contributors': set(),
|
||||
'performance_improvements': [],
|
||||
'security_fixes': []
|
||||
}
|
||||
|
||||
# 分析 Issue
|
||||
for issue in issues:
|
||||
labels = [label['name'] for label in issue.get('issue_tags', [])]
|
||||
subject = issue['subject']
|
||||
|
||||
if 'feature' in labels:
|
||||
changes['features'].append(format_issue_reference(issue))
|
||||
elif 'bug' in labels:
|
||||
changes['bug_fixes'].append(format_issue_reference(issue))
|
||||
elif 'enhancement' in labels:
|
||||
changes['enhancements'].append(format_issue_reference(issue))
|
||||
elif 'security' in labels:
|
||||
changes['security_fixes'].append(format_issue_reference(issue))
|
||||
|
||||
# 分析提交信息
|
||||
for commit in commits:
|
||||
message = commit['commit']['message']
|
||||
|
||||
# 使用 AI 分析提交消息
|
||||
analysis = analyze_commit_message(message)
|
||||
|
||||
if analysis.get('breaking_change'):
|
||||
changes['breaking_changes'].append(message)
|
||||
elif analysis.get('performance'):
|
||||
changes['performance_improvements'].append(message)
|
||||
|
||||
# 收集贡献者
|
||||
changes['contributors'].add(commit['author']['name'])
|
||||
|
||||
return changes
|
||||
|
||||
def format_release_notes(version, changes, prev_version):
|
||||
"""AI 生成结构化发布说明"""
|
||||
|
||||
notes = f"""# 🎉 Release {version}
|
||||
|
||||
## 📊 变更统计
|
||||
- **新功能**: {len(changes['features'])} 个
|
||||
- **Bug 修复**: {len(changes['bug_fixes'])} 个
|
||||
- **功能改进**: {len(changes['enhancements'])} 个
|
||||
- **破坏性变更**: {len(changes['breaking_changes'])} 个
|
||||
"""
|
||||
|
||||
if changes['features']:
|
||||
notes += "\n## ✨ 新功能\n"
|
||||
notes += "\n".join(f"- {feature}" for feature in changes['features'])
|
||||
notes += "\n"
|
||||
|
||||
if changes['bug_fixes']:
|
||||
notes += "\n## 🐛 Bug 修复\n"
|
||||
notes += "\n".join(f"- {fix}" for fix in changes['bug_fixes'])
|
||||
notes += "\n"
|
||||
|
||||
if changes['breaking_changes']:
|
||||
notes += "\n## ⚠️ 破坏性变更\n"
|
||||
notes += "\n".join(f"- {change}" for change in changes['breaking_changes'])
|
||||
notes += "\n"
|
||||
|
||||
if changes['contributors']:
|
||||
notes += f"\n## 🙏 贡献者\n"
|
||||
notes += ", ".join(sorted(changes['contributors']))
|
||||
notes += "\n"
|
||||
|
||||
notes += f"\n---\n**完整变更日志**: https://www.gitlink.org.cn/{owner}/{repo}/compare/{prev_version}...{version}"
|
||||
|
||||
return notes
|
||||
|
||||
def analyze_commit_message(message):
|
||||
"""AI 分析提交消息"""
|
||||
return {
|
||||
'breaking_change': bool(re.search(r'BREAKING|breaking|!', message)),
|
||||
'performance': bool(re.search(r'performance|优化|提升', message, re.I)),
|
||||
'security': bool(re.search(r'security|安全|漏洞', message, re.I))
|
||||
}
|
||||
```
|
||||
|
||||
## Release Notes 模板
|
||||
|
||||
### 标准模板
|
||||
|
||||
```markdown
|
||||
# 🎉 Release {VERSION}
|
||||
|
||||
## 📊 变更统计
|
||||
- **新功能**: {FEATURE_COUNT} 个
|
||||
- **Bug 修复**: {BUG_FIX_COUNT} 个
|
||||
- **功能改进**: {ENHANCEMENT_COUNT} 个
|
||||
- **破坏性变更**: {BREAKING_COUNT} 个
|
||||
|
||||
## ✨ 新功能
|
||||
{FEATURES_LIST}
|
||||
|
||||
## 🐛 Bug 修复
|
||||
{BUG_FIXES_LIST}
|
||||
|
||||
## 🔧 功能改进
|
||||
{ENHANCEMENTS_LIST}
|
||||
|
||||
## ⚠️ 破坏性变更
|
||||
{BREAKING_CHANGES_LIST}
|
||||
|
||||
## 🙏 贡献者
|
||||
{CONTRIBUTORS_LIST}
|
||||
|
||||
## 📥 安装
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm install {PACKAGE}@{VERSION}
|
||||
|
||||
# 使用 yarn
|
||||
yarn add {PACKAGE}@{VERSION}
|
||||
|
||||
# 使用 pnpm
|
||||
pnpm add {PACKAGE}@{VERSION}
|
||||
```
|
||||
|
||||
## 🔄 升级指南
|
||||
{UPGRADE_GUIDE}
|
||||
|
||||
## 📚 文档
|
||||
完整文档请查看: https://www.gitlink.org.cn/{OWNER}/{REPO}/wiki
|
||||
|
||||
---
|
||||
**完整变更日志**: https://www.gitlink.org.cn/{OWNER}/{REPO}/compare/{PREV_VERSION}...{VERSION}
|
||||
```
|
||||
|
||||
### 简化模板
|
||||
|
||||
```markdown
|
||||
# {VERSION}
|
||||
|
||||
## 新增
|
||||
{FEATURES}
|
||||
|
||||
## 修复
|
||||
{BUG_FIXES}
|
||||
|
||||
## 改进
|
||||
{ENHANCEMENTS}
|
||||
|
||||
## 贡献者
|
||||
{CONTRIBUTORS}
|
||||
|
||||
## 链接
|
||||
- [完整变更](https://www.gitlink.org.cn/{OWNER}/{REPO}/compare/{PREV_VERSION}...{VERSION})
|
||||
- [问题追踪](https://www.gitlink.org.cn/{OWNER}/{REPO}/issues)
|
||||
```
|
||||
|
||||
## 自动化分类规则
|
||||
|
||||
基于提交信息和 Issue 标签的自动分类:
|
||||
|
||||
```python
|
||||
RELEASE_CATEGORIES = {
|
||||
'features': {
|
||||
'labels': ['feature', 'enhancement'],
|
||||
'commit_keywords': ['feat:', 'add', 'new'],
|
||||
'icon': '✨',
|
||||
'title': '新功能'
|
||||
},
|
||||
'bug_fixes': {
|
||||
'labels': ['bug', 'fix'],
|
||||
'commit_keywords': ['fix:', 'bugfix'],
|
||||
'icon': '🐛',
|
||||
'title': 'Bug 修复'
|
||||
},
|
||||
'enhancements': {
|
||||
'labels': ['improvement', 'optimize'],
|
||||
'commit_keywords': ['improve:', 'optimize:', 'refactor:'],
|
||||
'icon': '🔧',
|
||||
'title': '功能改进'
|
||||
},
|
||||
'breaking_changes': {
|
||||
'labels': ['breaking', 'major'],
|
||||
'commit_keywords': ['BREAKING', 'breaking:', '!'],
|
||||
'icon': '⚠️',
|
||||
'title': '破坏性变更'
|
||||
},
|
||||
'security': {
|
||||
'labels': ['security', 'vulnerability'],
|
||||
'commit_keywords': ['security:', 'fix security'],
|
||||
'icon': '🔒',
|
||||
'title': '安全修复'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 版本号规范
|
||||
|
||||
遵循语义化版本 (Semantic Versioning):
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
MAJOR: 不兼容的 API 变更
|
||||
MINOR: 向后兼容的功能新增
|
||||
PATCH: 向后兼容的 Bug 修复
|
||||
```
|
||||
|
||||
版本号示例:
|
||||
- `1.0.0` → `1.1.0`:新增功能
|
||||
- `1.1.0` → `1.1.1`:Bug 修复
|
||||
- `1.1.1` → `2.0.0`:破坏性变更
|
||||
|
||||
## 质量检查
|
||||
|
||||
发布前检查清单:
|
||||
|
||||
- [ ] Release Notes 完整性检查
|
||||
- [ ] 变更统计准确性验证
|
||||
- [ ] 破坏性变更标识
|
||||
- [ ] 升级指南完整性
|
||||
- [ ] 文档链接正确性
|
||||
- [ ] 安装指令有效性
|
||||
- [ ] 贡献者列表完整性
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **定期发布**:建立定期发布节奏
|
||||
2. **变更追踪**:确保所有变更都被记录
|
||||
3. **清晰分类**:使用明确的分类和标签
|
||||
4. **用户友好**:提供升级指南和迁移说明
|
||||
5. **版本规范**:遵循语义化版本规范
|
||||
|
||||
## References
|
||||
|
||||
- [workflow-issue-triage](workflow-issue-triage.md) — Issue 分类工作流
|
||||
- [workflow-pr-review](workflow-pr-review.md) — PR 审查工作流
|
||||
- [gitlink-workflow](../SKILL.md) — 工作流总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [release +create](../../gitlink-release/references/gitlink-release-create.md) — 创建 Release
|
||||
- [release +list](../../gitlink-release/references/gitlink-release-list.md) — 列出 Release
|
||||
|
|
@ -0,0 +1,646 @@
|
|||
# Workflow: Repo Setup(仓库初始化)
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **AI Agent 工作流**:此工作流专为 AI Agent 设计,用于自动化仓库初始化设置。
|
||||
|
||||
AI Agent 自动创建新仓库并完成基础配置,包括分支保护、初始文档、Issue 模板等,确保新项目快速启动。
|
||||
|
||||
## 工作流概述
|
||||
|
||||
Repo Setup 工作流自动化创建新 GitLink 仓库,并进行标准化初始化配置,包括 README、LICENSE、分支保护、CI 配置等项目必需的设置。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- **项目创建**:快速创建新的项目仓库
|
||||
- **标准化设置**:确保所有仓库配置一致
|
||||
- **模板应用**:应用组织或团队的项目模板
|
||||
- **批量创建**:批量创建多个相关项目
|
||||
|
||||
## 工作流步骤
|
||||
|
||||
### 步骤 1:创建仓库
|
||||
|
||||
```bash
|
||||
# 创建新仓库
|
||||
gitlink-cli repo +create \
|
||||
--name my-awesome-project \
|
||||
--description "一个很棒的项目" \
|
||||
--private false
|
||||
|
||||
# 或者创建私有仓库
|
||||
gitlink-cli repo +create \
|
||||
--name internal-project \
|
||||
--description "内部项目" \
|
||||
--private true
|
||||
```
|
||||
|
||||
### 步骤 2:初始化本地仓库
|
||||
|
||||
```bash
|
||||
# 本地初始化
|
||||
cd my-awesome-project
|
||||
git init
|
||||
git remote add gitlink https://www.gitlink.org.cn/username/my-awesome-project.git
|
||||
|
||||
# 创建初始文件
|
||||
echo "# My Awesome Project" > README.md
|
||||
echo "MIT License" > LICENSE
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git push -u gitlink master:master
|
||||
```
|
||||
|
||||
### 步骤 3:设置分支保护
|
||||
|
||||
```bash
|
||||
# 保护主分支
|
||||
gitlink-cli branch +protect \
|
||||
--owner username \
|
||||
--repo my-awesome-project \
|
||||
--name master
|
||||
|
||||
# 如果使用 main 分支
|
||||
gitlink-cli branch +protect \
|
||||
--owner username \
|
||||
--repo my-awesome-project \
|
||||
--name main
|
||||
```
|
||||
|
||||
### 步骤 4:创建配置文件
|
||||
|
||||
```bash
|
||||
# 创建 .gitignore
|
||||
cat > .gitignore << 'EOF'
|
||||
# 依赖
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# 构建输出
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
.env.local
|
||||
EOF
|
||||
|
||||
# 创建配置文件(根据项目类型)
|
||||
if [ "$PROJECT_TYPE" = "node" ]; then
|
||||
echo '{"name":"my-awesome-project","version":"1.0.0"}' > package.json
|
||||
elif [ "$PROJECT_TYPE" = "python" ]; then
|
||||
echo "[project]\nname = 'my-awesome-project'\nversion = '1.0.0'" > pyproject.toml
|
||||
fi
|
||||
|
||||
git add .gitignore package.json pyproject.toml
|
||||
git commit -m "Add project configuration files"
|
||||
git push gitlink master:master
|
||||
```
|
||||
|
||||
### 步骤 5:创建 Issue 和 PR 模板
|
||||
|
||||
```bash
|
||||
# 创建 Issue 模板
|
||||
cat > .github/ISSUE_TEMPLATE/bug_report.md << 'EOF'
|
||||
---
|
||||
name: Bug 报告
|
||||
about: 报告项目中的问题
|
||||
title: '[Bug] '
|
||||
---
|
||||
|
||||
## Bug 描述
|
||||
简要描述遇到的问题。
|
||||
|
||||
## 复现步骤
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## 预期行为
|
||||
描述你期望发生的行为。
|
||||
|
||||
## 实际行为
|
||||
描述实际发生的行为。
|
||||
|
||||
## 环境
|
||||
- 操作系统:
|
||||
- 版本:
|
||||
- 其他信息:
|
||||
EOF
|
||||
|
||||
# 创建 PR 模板
|
||||
cat > .github/PULL_REQUEST_TEMPLATE.md << 'EOF'
|
||||
## 变更描述
|
||||
简要描述这个 PR 的目的和内容。
|
||||
|
||||
## 变更类型
|
||||
- [ ] Bug 修复
|
||||
- [ ] 新功能
|
||||
- [ ] 功能改进
|
||||
- [ ] 文档更新
|
||||
- [ ] 性能优化
|
||||
- [ ] 代码重构
|
||||
|
||||
## 测试
|
||||
描述你如何测试这些变更:
|
||||
|
||||
## 检查清单
|
||||
- [ ] 代码遵循项目规范
|
||||
- [ ] 已添加必要的测试
|
||||
- [ ] 已更新相关文档
|
||||
- [ ] 所有测试通过
|
||||
- [ ] 无合并冲突
|
||||
EOF
|
||||
|
||||
git add .github/
|
||||
git commit -m "Add issue and PR templates"
|
||||
git push gitlink master:master
|
||||
```
|
||||
|
||||
### 步骤 6:创建初始 Issue
|
||||
|
||||
```bash
|
||||
# 创建项目初始化 Issue
|
||||
gitlink-cli issue +create \
|
||||
--owner username \
|
||||
--repo my-awesome-project \
|
||||
--title "完成项目初始化" \
|
||||
--body "## 初始化任务清单
|
||||
|
||||
- [x] 创建仓库
|
||||
- [x] 添加 README.md
|
||||
- [x] 添加 LICENSE
|
||||
- [x] 设置分支保护
|
||||
- [x] 添加配置文件
|
||||
- [x] 创建 Issue 模板
|
||||
- [x] 创建 PR 模板
|
||||
- [ ] 配置 CI/CD
|
||||
- [ ] 添加项目文档
|
||||
- [ ] 设置开发指南
|
||||
|
||||
## 下一步
|
||||
1. 配置 CI/CD 流程
|
||||
2. 编写项目文档
|
||||
3. 设置开发环境指南
|
||||
4. 创建贡献指南"
|
||||
```
|
||||
|
||||
### 步骤 7:配置 CI/CD(可选)
|
||||
|
||||
```bash
|
||||
# 创建 CI 配置文件
|
||||
cat > .gitlab-ci.yml << 'EOF'
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- echo "Running tests..."
|
||||
- npm test
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- echo "Building..."
|
||||
- npm run build
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Deploying..."
|
||||
- npm run deploy
|
||||
only:
|
||||
- master
|
||||
EOF
|
||||
|
||||
git add .gitlab-ci.yml
|
||||
git commit -m "Add CI/CD configuration"
|
||||
git push gitlink master:master
|
||||
```
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# 仓库初始化自动化脚本
|
||||
|
||||
PROJECT_NAME=$1
|
||||
PROJECT_DESC=$2
|
||||
IS_PRIVATE=${3:-false}
|
||||
OWNER="username"
|
||||
|
||||
if [ -z "$PROJECT_NAME" ]; then
|
||||
echo "使用方法: $0 <project_name> [description] [private]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "开始初始化项目: $PROJECT_NAME"
|
||||
|
||||
# 1. 创建仓库
|
||||
echo "创建仓库..."
|
||||
REPO_INFO=$(gitlink-cli repo +create \
|
||||
--name "$PROJECT_NAME" \
|
||||
--description "$PROJECT_DESC" \
|
||||
--private "$IS_PRIVATE" \
|
||||
--format json)
|
||||
|
||||
if echo "$REPO_INFO" | jq -e '.ok' > /dev/null; then
|
||||
echo "✅ 仓库创建成功"
|
||||
else
|
||||
echo "❌ 仓库创建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 本地初始化
|
||||
echo "初始化本地仓库..."
|
||||
mkdir -p "$PROJECT_NAME"
|
||||
cd "$PROJECT_NAME"
|
||||
git init
|
||||
git remote add gitlink "https://www.gitlink.org.cn/$OWNER/$PROJECT_NAME.git"
|
||||
|
||||
# 3. 创建基础文件
|
||||
echo "创建项目文件..."
|
||||
|
||||
# README.md
|
||||
cat > README.md << EOF
|
||||
# $PROJECT_NAME
|
||||
|
||||
$PROJECT_DESC
|
||||
|
||||
## 快速开始
|
||||
|
||||
\`\`\`bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发模式运行
|
||||
npm run dev
|
||||
|
||||
# 构建项目
|
||||
npm run build
|
||||
|
||||
# 运行测试
|
||||
npm test
|
||||
\`\`\`
|
||||
|
||||
## 项目结构
|
||||
|
||||
\`\`\`
|
||||
$PROJECT_NAME/
|
||||
├── src/ # 源代码
|
||||
├── tests/ # 测试文件
|
||||
├── docs/ # 文档
|
||||
├── scripts/ # 脚本
|
||||
└── package.json # 项目配置
|
||||
\`\`\`
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
EOF
|
||||
|
||||
# LICENSE
|
||||
cat > LICENSE << 'EOF'
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
EOF
|
||||
|
||||
# package.json
|
||||
cat > package.json << EOF
|
||||
{
|
||||
"name": "$PROJECT_NAME",
|
||||
"version": "1.0.0",
|
||||
"description": "$PROJECT_DESC",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"dev": "echo 'Development mode'",
|
||||
"build": "echo 'Building project'",
|
||||
"test": "echo 'Running tests'",
|
||||
"lint": "echo 'Linting code'"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://www.gitlink.org.cn/$OWNER/$PROJECT_NAME.git"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# .gitignore
|
||||
cat > .gitignore << 'EOF'
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
.vscode/
|
||||
.idea/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
EOF
|
||||
|
||||
# 创建目录结构
|
||||
mkdir -p src tests docs scripts
|
||||
|
||||
# 4. 提交初始文件
|
||||
echo "提交初始文件..."
|
||||
git add .
|
||||
git commit -m "Initial commit"
|
||||
git push -u gitlink master:master
|
||||
|
||||
# 5. 设置分支保护
|
||||
echo "设置分支保护..."
|
||||
gitlink-cli branch +protect \
|
||||
--owner "$OWNER" \
|
||||
--repo "$PROJECT_NAME" \
|
||||
--name master
|
||||
|
||||
# 6. 创建项目模板
|
||||
echo "创建 Issue 模板..."
|
||||
mkdir -p .github/ISSUE_TEMPLATE
|
||||
|
||||
cat > .github/ISSUE_TEMPLATE/bug_report.md << 'EOF'
|
||||
---
|
||||
name: Bug 报告
|
||||
about: 报告项目中的问题
|
||||
title: '[Bug] '
|
||||
---
|
||||
|
||||
## Bug 描述
|
||||
简要描述遇到的问题。
|
||||
|
||||
## 复现步骤
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## 预期行为
|
||||
描述你期望发生的行为。
|
||||
|
||||
## 实际行为
|
||||
描述实际发生的行为。
|
||||
|
||||
## 环境
|
||||
- 操作系统:
|
||||
- 版本:
|
||||
- 其他信息:
|
||||
EOF
|
||||
|
||||
# 7. 创建初始化 Issue
|
||||
echo "创建初始化 Issue..."
|
||||
gitlink-cli issue +create \
|
||||
--owner "$OWNER" \
|
||||
--repo "$PROJECT_NAME" \
|
||||
--title "完成项目初始化设置" \
|
||||
--body "## 项目初始化任务
|
||||
|
||||
### 基础配置
|
||||
- [x] 创建仓库
|
||||
- [x] 添加 README.md
|
||||
- [x] 添加 LICENSE
|
||||
- [x] 设置分支保护
|
||||
- [x] 添加配置文件
|
||||
- [x] 创建目录结构
|
||||
|
||||
### 下一步任务
|
||||
- [ ] 配置 CI/CD
|
||||
- [ ] 编写项目文档
|
||||
- [ ] 设置开发指南
|
||||
- [ ] 创建贡献指南
|
||||
- [ ] 添加代码规范
|
||||
- [ ] 配置代码检查
|
||||
|
||||
## 开发环境设置
|
||||
\`\`\`bash
|
||||
# 克隆仓库
|
||||
git clone https://www.gitlink.org.cn/$OWNER/$PROJECT_NAME.git
|
||||
|
||||
# 安装依赖
|
||||
cd $PROJECT_NAME
|
||||
npm install
|
||||
|
||||
# 开发模式
|
||||
npm run dev
|
||||
\`\`\`
|
||||
|
||||
## 贡献流程
|
||||
1. Fork 本仓库
|
||||
2. 创建功能分支 (\`git checkout -b feature/AmazingFeature\`)
|
||||
3. 提交更改 (\`git commit -m 'Add some AmazingFeature'\`)
|
||||
4. 推送到分支 (\`git push origin feature/AmazingFeature\`)
|
||||
5. 创建 Pull Request"
|
||||
|
||||
echo "✅ 项目初始化完成!"
|
||||
echo ""
|
||||
echo "项目信息:"
|
||||
echo " 名称: $PROJECT_NAME"
|
||||
echo " 仓库: https://www.gitlink.org.cn/$OWNER/$PROJECT_NAME"
|
||||
echo " 状态: $([ "$IS_PRIVATE" = "true" ] && echo "私有" || echo "公开")"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo " 1. cd $PROJECT_NAME"
|
||||
echo " 2. 配置开发环境"
|
||||
echo " 3. 开始开发"
|
||||
```
|
||||
|
||||
## AI Agent 集成示例
|
||||
|
||||
Claude Code 等 AI Agent 可以深度集成此工作流:
|
||||
|
||||
```python
|
||||
# AI Agent 仓库初始化
|
||||
def setup_repository(owner, repo_name, description, private=False):
|
||||
"""AI Agent 自动化仓库初始化"""
|
||||
|
||||
# 1. 创建仓库
|
||||
repo = create_repository(owner, repo_name, description, private)
|
||||
|
||||
# 2. 初始化项目结构
|
||||
project_structure = generate_project_structure(repo_name)
|
||||
initialize_project_files(repo_name, project_structure)
|
||||
|
||||
# 3. 配置分支保护
|
||||
protect_branch(owner, repo_name, 'master')
|
||||
|
||||
# 4. 创建 Issue 模板
|
||||
create_issue_templates(owner, repo_name)
|
||||
|
||||
# 5. 配置 CI/CD
|
||||
setup_cicd(owner, repo_name, project_structure['type'])
|
||||
|
||||
# 6. 创建初始化 Issue
|
||||
create_setup_issue(owner, repo_name)
|
||||
|
||||
return repo
|
||||
|
||||
def generate_project_structure(repo_name):
|
||||
"""AI 生成项目结构"""
|
||||
|
||||
# 分析项目名称和描述,确定项目类型
|
||||
project_type = analyze_project_type(repo_name)
|
||||
|
||||
structures = {
|
||||
'node': {
|
||||
'directories': ['src', 'tests', 'docs', 'scripts'],
|
||||
'files': {
|
||||
'package.json': generate_package_json(repo_name),
|
||||
'.gitignore': generate_gitignore('node'),
|
||||
'README.md': generate_readme(repo_name)
|
||||
}
|
||||
},
|
||||
'python': {
|
||||
'directories': ['src', 'tests', 'docs', 'scripts'],
|
||||
'files': {
|
||||
'pyproject.toml': generate_pyproject(repo_name),
|
||||
'.gitignore': generate_gitignore('python'),
|
||||
'README.md': generate_readme(repo_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return structures.get(project_type, structures['node'])
|
||||
|
||||
def create_issue_templates(owner, repo):
|
||||
"""创建 Issue 模板"""
|
||||
|
||||
templates = {
|
||||
'bug_report.md': generate_bug_template(),
|
||||
'feature_request.md': generate_feature_template(),
|
||||
'question.md': generate_question_template()
|
||||
}
|
||||
|
||||
for template_name, content in templates.items():
|
||||
# 使用 API 创建模板文件
|
||||
create_file_via_api(owner, repo,
|
||||
f'.github/ISSUE_TEMPLATE/{template_name}',
|
||||
content)
|
||||
```
|
||||
|
||||
## 项目模板
|
||||
|
||||
### 前端项目模板
|
||||
|
||||
```bash
|
||||
# 创建前端项目结构
|
||||
create_frontend_project() {
|
||||
mkdir -p src/{components,pages,hooks,utils}
|
||||
mkdir -p public/{images,fonts}
|
||||
mkdir -p tests/{unit,integration}
|
||||
|
||||
# package.json
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "my-frontend-project",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"test": "vitest",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
### 后端项目模板
|
||||
|
||||
```bash
|
||||
# 创建后端项目结构
|
||||
create_backend_project() {
|
||||
mkdir -p src/{controllers,models,routes,middleware}
|
||||
mkdir -p tests/{unit,integration}
|
||||
mkdir -p config
|
||||
mkdir -m migrations
|
||||
|
||||
# package.json
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "my-backend-project",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test": "jest",
|
||||
"migrate": "knex migrate:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.0",
|
||||
"knex": "^2.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
## 配置检查清单
|
||||
|
||||
仓库初始化完成后检查:
|
||||
|
||||
- [ ] 仓库创建成功
|
||||
- [ ] README.md 完整
|
||||
- [ ] LICENSE 文件存在
|
||||
- [ ] 分支保护已设置
|
||||
- [ ] .gitignore 配置正确
|
||||
- [ ] Issue 模板创建
|
||||
- [ ] PR 模板创建
|
||||
- [ ] CI/CD 配置(可选)
|
||||
- [ ] 初始化 Issue 已创建
|
||||
- [ ] 本地仓库可正常推送
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **标准化模板**:使用统一的项目模板
|
||||
2. **配置管理**:统一配置文件格式
|
||||
3. **文档完整**:确保 README 和文档完整
|
||||
4. **安全设置**:合理设置分支保护
|
||||
5. **CI/CD 配置**:早期建立 CI/CD 流程
|
||||
|
||||
## References
|
||||
|
||||
- [workflow-issue-triage](workflow-issue-triage.md) — Issue 分类工作流
|
||||
- [workflow-sprint-report](workflow-sprint-report.md) — Sprint 报告工作流
|
||||
- [gitlink-workflow](../SKILL.md) — 工作流总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [repo +create](../../gitlink-repo/references/repo-create.md) — 创建仓库
|
||||
- [branch +protect](../../gitlink-branch/references/branch-protect.md) — 保护分支
|
||||
|
|
@ -0,0 +1,549 @@
|
|||
# Workflow: Sprint Report(Sprint 报告)
|
||||
|
||||
> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
> **AI Agent 工作流**:此工作流专为 AI Agent 设计,用于自动生成 Sprint 进度报告。
|
||||
|
||||
AI Agent 自动汇总 Sprint 期间的 Issue、PR、提交记录等数据,生成结构化的进度报告,帮助团队了解项目进展。
|
||||
|
||||
## 工作流概述
|
||||
|
||||
Sprint Report 工作流收集 Sprint 期间的所有活动数据,包括 Issue 完成情况、PR 合并状态、代码提交统计、团队成员贡献等,自动生成团队进度报告。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- **Sprint 回顾**:为 Sprint 回顾会议提供数据支持
|
||||
- **进度汇报**:向管理层汇报项目进展
|
||||
- **团队协调**:协调团队工作计划和资源分配
|
||||
- **绩效评估**:评估团队和个人的工作效率
|
||||
|
||||
## 工作流步骤
|
||||
|
||||
### 步骤 1:确定报告时间范围
|
||||
|
||||
```bash
|
||||
# 设置 Sprint 时间范围(通常为 2 周)
|
||||
SPRINT_START="2026-01-01"
|
||||
SPRINT_END="2026-01-14"
|
||||
|
||||
# 或者基于当前时间计算
|
||||
SPRINT_END=$(date +%Y-%m-%d)
|
||||
SPRINT_START=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
```
|
||||
|
||||
### 步骤 2:获取 Issue 统计
|
||||
|
||||
```bash
|
||||
# 获取 Sprint 期间关闭的 Issue
|
||||
CLOSED_ISSUES=$(gitlink-cli issue +list --state closed --format json | \
|
||||
jq '.data.issues[] |
|
||||
select(.closed_at >= "'$SPRINT_START'" and .closed_at <= "'$SPRINT_END'")')
|
||||
|
||||
# 获取新增 Issue
|
||||
NEW_ISSUES=$(gitlink-cli issue +list --state open --format json | \
|
||||
jq '.data.issues[] |
|
||||
select(.created_at >= "'$SPRINT_START'" and .created_at <= "'$SPRINT_END'")')
|
||||
|
||||
# 统计 Issue 数据
|
||||
CLOSED_COUNT=$(echo "$CLOSED_ISSUES" | jq -s 'length')
|
||||
NEW_COUNT=$(echo "$NEW_ISSUES" | jq -s 'length')
|
||||
```
|
||||
|
||||
### 步骤 3:获取 PR 统计
|
||||
|
||||
```bash
|
||||
# 获取 Sprint 期间合并的 PR
|
||||
MERGED_PRS=$(gitlink-cli pr +list --state merged --format json | \
|
||||
jq '.data.prs[] |
|
||||
select(.merged_at >= "'$SPRINT_START'" and .merged_at <= "'$SPRINT_END'")')
|
||||
|
||||
# 获取新建的 PR
|
||||
NEW_PRS=$(gitlink-cli pr +list --state open --format json | \
|
||||
jq '.data.prs[] |
|
||||
select(.created_at >= "'$SPRINT_START'" and .created_at <= "'$SPRINT_END'")')
|
||||
|
||||
# 统计 PR 数据
|
||||
MERGED_COUNT=$(echo "$MERGED_PRS" | jq -s 'length')
|
||||
NEW_PR_COUNT=$(echo "$NEW_PRS" | jq -s 'length')
|
||||
```
|
||||
|
||||
### 步骤 4:获取提交统计
|
||||
|
||||
```bash
|
||||
# 获取项目活动数据
|
||||
ACTIVITY=$(gitlink-cli api GET /:owner/:repo/activity --format json | \
|
||||
jq ".data.activity[] |
|
||||
select(.created_at >= \"$SPRINT_START\" and .created_at <= \"$SPRINT_END\")")
|
||||
|
||||
# 提取提交数据
|
||||
COMMITS=$(echo "$ACTIVITY" | jq 'select(.type == "commit")')
|
||||
COMMIT_COUNT=$(echo "$COMMITS" | jq -s 'length')
|
||||
```
|
||||
|
||||
### 步骤 5:分析团队贡献
|
||||
|
||||
```bash
|
||||
# 按团队成员统计贡献
|
||||
CONTRIBUTORS=$(gitlink-cli api GET /:owner/:repo/activity --format json | \
|
||||
jq ".data.activity[] |
|
||||
select(.created_at >= \"$SPRINT_START\" and .created_at <= \"$SPRINT_END\") |
|
||||
.author |
|
||||
group_by(.) |
|
||||
map({developer: .[0], count: length}) |
|
||||
sort_by(.count) | reverse")
|
||||
```
|
||||
|
||||
### 步骤 6:生成 Sprint 报告
|
||||
|
||||
```bash
|
||||
# 生成结构化的 Sprint 报告
|
||||
SPRINT_REPORT="# 📊 Sprint 进度报告
|
||||
|
||||
**时间范围**: $SPRINT_START 至 $SPRINT_END
|
||||
**Sprint 周期**: 14 天
|
||||
|
||||
## 🎯 目标达成情况
|
||||
|
||||
### Issue 统计
|
||||
- ✅ **完成 Issue**: $CLOSED_COUNT 个
|
||||
- 🆕 **新增 Issue**: $NEW_COUNT 个
|
||||
- 📈 **完成率**: $(($CLOSED_COUNT * 100 / ($CLOSED_COUNT + $NEW_COUNT)))%
|
||||
|
||||
### Pull Request 统计
|
||||
- 🔀 **合并 PR**: $MERGED_COUNT 个
|
||||
- 🆕 **新建 PR**: $NEW_PR_COUNT 个
|
||||
- ✅ **合并率**: $(($MERGED_COUNT * 100 / ($MERGED_COUNT + $NEW_PR_COUNT)))%
|
||||
|
||||
### 代码提交统计
|
||||
- 💻 **提交次数**: $COMMIT_COUNT 次
|
||||
- 📊 **日均提交**: $(($COMMIT_COUNT / 14)) 次/天
|
||||
|
||||
## 👥 团队贡献
|
||||
$(echo "$CONTRIBUTORS" | jq -r '.[] | "- **\(.developer)**: \(.count) 次贡献"')
|
||||
|
||||
## 🎉 主要成就
|
||||
$(echo "$CLOSED_ISSUES" | jq -r '.[] | "- 完成 Issue: \(.subject)"')
|
||||
|
||||
## 🔄 进行中工作
|
||||
$(echo "$NEW_ISSUES" | jq -r '.[] | "- 新建 Issue: \(.subject)"')
|
||||
|
||||
## 📈 下期计划
|
||||
1. 继续进行中的 Issue 开发
|
||||
2. 新功能规划和设计
|
||||
3. 技术债务清理
|
||||
4. 性能优化工作"
|
||||
|
||||
# 输出报告
|
||||
echo "$SPRINT_REPORT"
|
||||
```
|
||||
|
||||
## 完整工作流示例
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Sprint 报告自动化生成脚本
|
||||
|
||||
OWNER="username"
|
||||
REPO="myproject"
|
||||
REPORT_DIR="sprint_reports"
|
||||
|
||||
# 获取时间范围
|
||||
SPRINT_NUMBER=$1
|
||||
if [ -z "$SPRINT_NUMBER" ]; then
|
||||
# 计算当前是第几个 Sprint(假设每 Sprint 2 周,从项目开始计算)
|
||||
PROJECT_START="2026-01-01"
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
DAYS_DIFF=$(( ($(date -d "$CURRENT_DATE" +%s) - $(date -d "$PROJECT_START" +%s)) / 86400 ))
|
||||
SPRINT_NUMBER=$((DAYS_DIFF / 14 + 1))
|
||||
fi
|
||||
|
||||
SPRINT_START=$(date -d "$((SPRINT_NUMBER - 1)) weeks ago" +%Y-%m-%d)
|
||||
SPRINT_END=$(date -d "$((SPRINT_NUMBER - 1)) weeks ago +14 days" +%Y-%m-%d)
|
||||
|
||||
echo "生成 Sprint $SPRINT_NUMBER 报告 ($SPRINT_START - $SPRINT_END)"
|
||||
|
||||
# 创建报告目录
|
||||
mkdir -p "$REPORT_DIR"
|
||||
|
||||
# 1. 获取 Issue 数据
|
||||
echo "收集 Issue 数据..."
|
||||
ISSUE_DATA=$(gitlink-cli issue +list --owner $OWNER --repo $REPO --format json)
|
||||
|
||||
CLOSED_ISSUES=$(echo "$ISSUE_DATA" | jq -r ".data.issues[] |
|
||||
select(.closed_at >= \"$SPRINT_START\" and .closed_at <= \"$SPRINT_END\")")
|
||||
|
||||
NEW_ISSUES=$(echo "$ISSUE_DATA" | jq -r ".data.issues[] |
|
||||
select(.created_at >= \"$SPRINT_START\" and .created_at <= \"$SPRINT_END\")")
|
||||
|
||||
CLOSED_COUNT=$(echo "$CLOSED_ISSUES" | jq -s 'length')
|
||||
NEW_COUNT=$(echo "$NEW_ISSUES" | jq -s 'length')
|
||||
|
||||
# 2. 获取 PR 数据
|
||||
echo "收集 PR 数据..."
|
||||
PR_DATA=$(gitlink-cli pr +list --owner $OWNER --repo $REPO --format json)
|
||||
|
||||
MERGED_PRS=$(echo "$PR_DATA" | jq -r ".data.prs[] |
|
||||
select(.merged_at >= \"$SPRINT_START\" and .merged_at <= \"$SPRINT_END\")")
|
||||
|
||||
NEW_PRS=$(echo "$PR_DATA" | jq -r ".data.prs[] |
|
||||
select(.created_at >= \"$SPRINT_START\" and .created_at <= \"$SPRINT_END\")")
|
||||
|
||||
MERGED_COUNT=$(echo "$MERGED_PRS" | jq -s 'length')
|
||||
NEW_PR_COUNT=$(echo "$NEW_PRS" | jq -s 'length')
|
||||
|
||||
# 3. 获取提交数据
|
||||
echo "收集提交数据..."
|
||||
COMMITS=$(gitlink-cli api GET "/$OWNER/$REPO/commits" --format json | \
|
||||
jq -r ".data[] |
|
||||
select(.committed_date >= \"$SPRINT_START\" and .committed_date <= \"$SPRINT_END\")")
|
||||
|
||||
COMMIT_COUNT=$(echo "$COMMITS" | jq -s 'length')
|
||||
|
||||
# 4. 分析团队贡献
|
||||
echo "分析团队贡献..."
|
||||
CONTRIBUTORS=$(echo "$COMMITS" | jq -r '.author | group_by(.) |
|
||||
map({developer: .[0], count: length}) |
|
||||
sort_by(.count) | reverse')
|
||||
|
||||
# 5. 分析 Issue 标签
|
||||
echo "分析 Issue 分类..."
|
||||
FEATURES=$(echo "$CLOSED_ISSUES" | jq -r '[.[] | select(.issue_tags[]?.name == "feature")] | length')
|
||||
BUGS=$(echo "$CLOSED_ISSUES" | jq -r '[.[] | select(.issue_tags[]?.name == "bug")] | length')
|
||||
ENHANCEMENTS=$(echo "$CLOSED_ISSUES" | jq -r '[.[] | select(.issue_tags[]?.name == "enhancement")] | length')
|
||||
|
||||
# 6. 计算完成率
|
||||
COMPLETION_RATE=0
|
||||
if [ $((CLOSED_COUNT + NEW_COUNT)) -gt 0 ]; then
|
||||
COMPLETION_RATE=$((CLOSED_COUNT * 100 / (CLOSED_COUNT + NEW_COUNT)))
|
||||
fi
|
||||
|
||||
# 7. 生成报告
|
||||
echo "生成 Sprint 报告..."
|
||||
REPORT_FILE="$REPORT_DIR/sprint_${SPRINT_NUMBER}_$(date +%Y%m%d).md"
|
||||
|
||||
cat > "$REPORT_FILE" << EOF
|
||||
# 📊 Sprint $SPRINT_NUMBER 进度报告
|
||||
|
||||
**时间范围**: $SPRINT_START 至 $SPRINT_END
|
||||
**生成时间**: $(date +%Y-%m-%d)
|
||||
**报告周期**: 14 天
|
||||
|
||||
## 🎯 Sprint 目标达成情况
|
||||
|
||||
### 总体概览
|
||||
| 指标 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| ✅ 完成 Issue | $CLOSED_COUNT 个 | Sprint 期间关闭的 Issue |
|
||||
| 🆕 新增 Issue | $NEW_COUNT 个 | Sprint 期间新建的 Issue |
|
||||
| 🔀 合并 PR | $MERGED_COUNT 个 | Sprint 期间合并的 PR |
|
||||
| 🆕 新建 PR | $NEW_PR_COUNT 个 | Sprint 期间新建的 PR |
|
||||
| 💻 代码提交 | $COMMIT_COUNT 次 | Sprint 期间的提交次数 |
|
||||
|
||||
### 完成率分析
|
||||
- **Issue 完成率**: ${COMPLETION_RATE}%
|
||||
- **PR 合并率**: $((MERGED_COUNT * 100 / (MERGED_COUNT + NEW_PR_COUNT)))%
|
||||
- **平均日提交**: $((COMMIT_COUNT / 14)) 次/天
|
||||
|
||||
## 📊 Issue 分类统计
|
||||
|
||||
| 分类 | 数量 | 占比 |
|
||||
|------|------|------|
|
||||
| 新功能 | $FEATURES 个 | $((FEATURES * 100 / CLOSED_COUNT))% |
|
||||
| Bug 修复 | $BUGS 个 | $((BUGS * 100 / CLOSED_COUNT))% |
|
||||
| 功能改进 | $ENHANCEMENTS 个 | $((ENHANCEMENTS * 100 / CLOSED_COUNT))% |
|
||||
|
||||
## 👥 团队贡献统计
|
||||
|
||||
$(echo "$CONTRIBUTORS" | jq -r '.[] | |
|
||||
"| **\(.developer)** | \(.count) 次提交 | $((.count * 100 / COMMIT_COUNT))% |"')
|
||||
|
||||
## 🎉 主要成就
|
||||
|
||||
### 完成的 Issue
|
||||
$(echo "$CLOSED_ISSUES" | jq -r '"- [\(.subject)](#issue/\(.id)) - \(.assigned_to // "未分配")"')
|
||||
|
||||
### 合并的 PR
|
||||
$(echo "$MERGED_PRS" | jq -r '"- [\(.title)](#pr/\(.id)) - \(.author.login)"')
|
||||
|
||||
## 🔄 进行中的工作
|
||||
|
||||
### 未完成的 Issue
|
||||
$(echo "$NEW_ISSUES" | jq -r '"- [\(.subject)](#issue/\(.id)) - \(.assigned_to // "未分配")"')
|
||||
|
||||
### 待合并的 PR
|
||||
$(echo "$NEW_PRS" | jq -r '"- [\(.title)](#pr/\(.id)) - \(.author.login)"')
|
||||
|
||||
## 📈 趋势分析
|
||||
|
||||
### 代码活动趋势
|
||||
- 本 Sprint 共有 **$COMMIT_COUNT 次提交**,日均 **$((COMMIT_COUNT / 14)) 次**
|
||||
- 比上 Sprint $([[ $SPRINT_NUMBER -gt 1 ]] && echo "增长了/减少了 XX%" || echo "为基线数据")
|
||||
|
||||
### 团队效率分析
|
||||
- 团队成员积极参与,贡献分布较为均匀
|
||||
- 代码审查及时,PR 合并率良好
|
||||
|
||||
## ⚠️ 风险和问题
|
||||
|
||||
### 当前风险
|
||||
- 高优先级 Issue 积压:$(echo "$NEW_ISSUES" | jq '[.[] | select(.priority_id == 1)] | length') 个
|
||||
- 长期未解决的 Issue:$(echo "$NEW_ISSUES" | jq '[.[] | select(.created_at < "'$SPRINT_START'")] | length') 个
|
||||
|
||||
### 技术债务
|
||||
- 代码复用待改进
|
||||
- 测试覆盖率需要提升
|
||||
- 文档需要更新
|
||||
|
||||
## 📋 下期计划
|
||||
|
||||
### 主要目标
|
||||
1. 继续完成当前进行中的 Issue
|
||||
2. 优化代码质量和测试覆盖
|
||||
3. 更新项目文档
|
||||
4. 技术债务清理
|
||||
|
||||
### 资源规划
|
||||
- 开发资源:保持当前团队配置
|
||||
- 时间规划:重点关注高优先级 Issue
|
||||
- 技术重点:性能优化和代码重构
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有团队成员在 Sprint $SPRINT_NUMBER 期间的辛勤工作!
|
||||
|
||||
---
|
||||
**报告生成**: $(date +%Y-%m-%d %H:%M:%S)
|
||||
**数据来源**: GitLink API
|
||||
**报告类型**: 自动化 Sprint 报告
|
||||
EOF
|
||||
|
||||
echo "✅ Sprint 报告已生成: $REPORT_FILE"
|
||||
|
||||
# 8. 可选:创建 Issue 讨论报告
|
||||
echo "创建 Sprint 回顾 Issue..."
|
||||
REVIEW_ISSUE_BODY="## Sprint $SPRINT_NUMBER 回顾
|
||||
|
||||
### Sprint 报告
|
||||
完整的 Sprint 报告请查看: [Sprint $SPRINT_NUMBER 报告](../../blob/master/$REPORT_FILE)
|
||||
|
||||
### 讨论要点
|
||||
1. 目标达成情况分析
|
||||
2. 团队协作效果评估
|
||||
3. 流程改进建议
|
||||
4. 下 Sprint 目标规划
|
||||
|
||||
### 问题跟踪
|
||||
- 需要解决的问题
|
||||
- 改进建议
|
||||
- 风险识别"
|
||||
|
||||
gitlink-cli issue +create \
|
||||
--owner $OWNER \
|
||||
--repo $REPO \
|
||||
--title "Sprint $SPRINT_NUMBER 回顾" \
|
||||
--body "$REVIEW_ISSUE_BODY"
|
||||
|
||||
echo "Sprint 报告工作流完成!"
|
||||
```
|
||||
|
||||
## AI Agent 集成示例
|
||||
|
||||
Claude Code 等 AI Agent 可以深度集成此工作流:
|
||||
|
||||
```python
|
||||
# AI Agent 生成 Sprint 报告
|
||||
def generate_sprint_report(owner, repo, sprint_number):
|
||||
"""AI Agent 自动生成 Sprint 进度报告"""
|
||||
|
||||
# 1. 确定 Sprint 时间范围
|
||||
sprint_start, sprint_end = calculate_sprint_period(sprint_number)
|
||||
|
||||
# 2. 收集数据
|
||||
sprint_data = collect_sprint_data(owner, repo, sprint_start, sprint_end)
|
||||
|
||||
# 3. AI 分析数据
|
||||
analysis = analyze_sprint_performance(sprint_data)
|
||||
|
||||
# 4. 生成报告
|
||||
report = generate_report_content(sprint_number, sprint_data, analysis)
|
||||
|
||||
# 5. 保存报告并创建回顾 Issue
|
||||
save_report(report, sprint_number)
|
||||
create_review_issue(owner, repo, sprint_number, report)
|
||||
|
||||
return report
|
||||
|
||||
def collect_sprint_data(owner, repo, start_date, end_date):
|
||||
"""收集 Sprint 数据"""
|
||||
|
||||
return {
|
||||
'issues': {
|
||||
'closed': get_closed_issues(owner, repo, start_date, end_date),
|
||||
'new': get_new_issues(owner, repo, start_date, end_date)
|
||||
},
|
||||
'pull_requests': {
|
||||
'merged': get_merged_prs(owner, repo, start_date, end_date),
|
||||
'new': get_new_prs(owner, repo, start_date, end_date)
|
||||
},
|
||||
'commits': get_commits(owner, repo, start_date, end_date),
|
||||
'contributors': get_contributor_stats(owner, repo, start_date, end_date)
|
||||
}
|
||||
|
||||
def analyze_sprint_performance(data):
|
||||
"""AI 分析 Sprint 表现"""
|
||||
|
||||
analysis = {
|
||||
'velocity': calculate_velocity(data),
|
||||
'trends': identify_trends(data),
|
||||
'risks': identify_risks(data),
|
||||
'recommendations': generate_recommendations(data)
|
||||
}
|
||||
|
||||
# AI 分析完成率趋势
|
||||
completion_rate = len(data['issues']['closed']) / (
|
||||
len(data['issues']['closed']) + len(data['issues']['new'])
|
||||
) * 100
|
||||
|
||||
if completion_rate > 80:
|
||||
analysis['performance'] = 'excellent'
|
||||
elif completion_rate > 60:
|
||||
analysis['performance'] = 'good'
|
||||
else:
|
||||
analysis['performance'] = 'needs_improvement'
|
||||
|
||||
return analysis
|
||||
|
||||
def generate_report_content(sprint_number, data, analysis):
|
||||
"""AI 生成报告内容"""
|
||||
|
||||
report = f"""# 📊 Sprint {sprint_number} 进度报告
|
||||
|
||||
## 🎯 目标达成情况
|
||||
|
||||
### 总体概览
|
||||
- **完成 Issue**: {len(data['issues']['closed'])} 个
|
||||
- **新增 Issue**: {len(data['issues']['new'])} 个
|
||||
- **合并 PR**: {len(data['pull_requests']['merged'])} 个
|
||||
- **代码提交**: {len(data['commits'])} 次
|
||||
|
||||
### AI 分析结果
|
||||
- **表现评级**: {analysis['performance']}
|
||||
- **团队速度**: {analysis['velocity']} story points
|
||||
- **主要趋势**: {analysis['trends']}
|
||||
|
||||
## 🎉 主要成就
|
||||
"""
|
||||
|
||||
# 添加主要成就
|
||||
for issue in data['issues']['closed'][:5]:
|
||||
report += f"- {issue['subject']} (#{issue['id']})\n"
|
||||
|
||||
# 添加风险和建议
|
||||
report += "\n## ⚠️ 风险识别\n"
|
||||
for risk in analysis['risks']:
|
||||
report += f"- {risk}\n"
|
||||
|
||||
report += "\n## 💡 改进建议\n"
|
||||
for recommendation in analysis['recommendations']:
|
||||
report += f"- {recommendation}\n"
|
||||
|
||||
return report
|
||||
```
|
||||
|
||||
## 报告模板
|
||||
|
||||
### 标准报告结构
|
||||
|
||||
```markdown
|
||||
# Sprint {NUMBER} 进度报告
|
||||
|
||||
## 元信息
|
||||
- **时间范围**: {START_DATE} - {END_DATE}
|
||||
- **Sprint 周期**: 14 天
|
||||
- **生成时间**: {TIMESTAMP}
|
||||
|
||||
## 目标达成
|
||||
### 完成情况
|
||||
- 计划完成: X 个 Issue
|
||||
- 实际完成: Y 个 Issue
|
||||
- 完成率: Z%
|
||||
|
||||
## 工作统计
|
||||
### Issue 统计
|
||||
- 关闭: N 个
|
||||
- 新建: M 个
|
||||
- 分类统计
|
||||
|
||||
### PR 统计
|
||||
- 合并: N 个
|
||||
- 新建: M 个
|
||||
- 合并率: X%
|
||||
|
||||
### 提交统计
|
||||
- 总提交: N 次
|
||||
- 日均: X 次
|
||||
|
||||
## 团队贡献
|
||||
- 成员A: N 次贡献
|
||||
- 成员B: M 次贡献
|
||||
|
||||
## 风险和问题
|
||||
- 当前风险
|
||||
- 技术债务
|
||||
- 阻塞问题
|
||||
|
||||
## 下期计划
|
||||
- 主要目标
|
||||
- 资源规划
|
||||
- 时间安排
|
||||
```
|
||||
|
||||
## 数据分析维度
|
||||
|
||||
### 1. 速度分析
|
||||
- Story Points 完成
|
||||
- Issue 完成数量
|
||||
- PR 合并数量
|
||||
|
||||
### 2. 质量分析
|
||||
- Bug 修复比例
|
||||
- 代码审查通过率
|
||||
- 测试覆盖率变化
|
||||
|
||||
### 3. 效率分析
|
||||
- 平均 Issue 解决时间
|
||||
- 平均 PR 合并时间
|
||||
- 代码审查周期
|
||||
|
||||
### 4. 团队分析
|
||||
- 成员贡献分布
|
||||
- 协作效率
|
||||
- 沟通成本
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **定期生成**:每个 Sprint 结束后及时生成报告
|
||||
2. **数据准确**:确保收集的数据完整准确
|
||||
3. **客观分析**:基于数据进行客观分析
|
||||
4. **行动导向**:报告应包含可执行的改进建议
|
||||
5. **团队参与**:让团队成员参与报告讨论
|
||||
|
||||
## 质量保证
|
||||
|
||||
报告质量检查:
|
||||
|
||||
- [ ] 数据完整性检查
|
||||
- [ ] 计算准确性验证
|
||||
- [ ] 格式一致性检查
|
||||
- [ ] 语法和拼写检查
|
||||
- [ ] 链接有效性验证
|
||||
- [ ] 客观性审查
|
||||
|
||||
## References
|
||||
|
||||
- [workflow-issue-triage](workflow-issue-triage.md) — Issue 分类工作流
|
||||
- [workflow-pr-review](workflow-pr-review.md) — PR 审查工作流
|
||||
- [gitlink-workflow](../SKILL.md) — 工作流总览
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md) — 认证和全局参数
|
||||
- [pm-sprint](../../gitlink-pm/references/pm-sprint.md) — Sprint 管理
|
||||
- [pm-report](../../gitlink-pm/references/pm-report.md) — 周报生成
|
||||
Loading…
Reference in New Issue