feat(ci): add repository CI control shortcuts
This commit is contained in:
parent
71ca2bb683
commit
0806f3096c
15
README.md
15
README.md
|
|
@ -477,13 +477,22 @@ gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --d
|
|||
|
||||
```bash
|
||||
# List builds
|
||||
gitlink-cli ci +list --owner Gitlink --repo forgeplus
|
||||
gitlink-cli ci +builds --owner Gitlink --repo forgeplus
|
||||
|
||||
# View build log
|
||||
gitlink-cli ci +log --owner Gitlink --repo forgeplus -i <build_id>
|
||||
gitlink-cli ci +logs --owner Gitlink --repo forgeplus --build <build_id>
|
||||
|
||||
# Restart a build
|
||||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i <build_id>
|
||||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus --build <build_id>
|
||||
|
||||
# Stop a build
|
||||
gitlink-cli ci +stop --owner Gitlink --repo forgeplus --build <build_id>
|
||||
|
||||
# Check CI authorization and safely toggle repository CI
|
||||
gitlink-cli ci +authorize --owner Gitlink --repo forgeplus
|
||||
gitlink-cli ci +activate --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli ci +activate --owner Gitlink --repo forgeplus --yes
|
||||
gitlink-cli ci +deactivate --owner Gitlink --repo forgeplus --dry-run
|
||||
```
|
||||
|
||||
### Pipeline Operations
|
||||
|
|
|
|||
|
|
@ -464,6 +464,26 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b
|
|||
gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --dry-run
|
||||
```
|
||||
|
||||
### CI/CD 操作
|
||||
|
||||
```bash
|
||||
# 查看构建列表
|
||||
gitlink-cli ci +builds --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看构建日志
|
||||
gitlink-cli ci +logs --owner Gitlink --repo forgeplus --build <build_id>
|
||||
|
||||
# 重启或停止构建
|
||||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus --build <build_id>
|
||||
gitlink-cli ci +stop --owner Gitlink --repo forgeplus --build <build_id>
|
||||
|
||||
# 查看 CI 授权状态并安全启停仓库 CI
|
||||
gitlink-cli ci +authorize --owner Gitlink --repo forgeplus
|
||||
gitlink-cli ci +activate --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli ci +activate --owner Gitlink --repo forgeplus --yes
|
||||
gitlink-cli ci +deactivate --owner Gitlink --repo forgeplus --dry-run
|
||||
```
|
||||
|
||||
### 流水线管理
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
# CI control shortcuts
|
||||
|
||||
## Background
|
||||
|
||||
The CI shortcut group already supported build listing, log inspection, restart,
|
||||
and stop operations. Repository-level CI activation, deactivation, and
|
||||
authorization checks were still documented as Raw API calls in `gitlink-ci`.
|
||||
|
||||
This change adds first-class CI control shortcuts.
|
||||
|
||||
## New shortcuts
|
||||
|
||||
- `ci +activate` activates CI for a repository.
|
||||
- `ci +deactivate` deactivates CI for a repository.
|
||||
- `ci +authorize` shows CI authorization state for a repository.
|
||||
|
||||
## Safety model
|
||||
|
||||
`ci +authorize` is read-only and can run directly:
|
||||
|
||||
```bash
|
||||
gitlink-cli ci +authorize --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
`ci +activate` and `ci +deactivate` change repository CI state, so they require
|
||||
an explicit confirmation flag and support dry-run previews:
|
||||
|
||||
```bash
|
||||
gitlink-cli ci +activate --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli ci +activate --owner Gitlink --repo forgeplus --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
gitlink-cli ci +deactivate --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli ci +deactivate --owner Gitlink --repo forgeplus --yes
|
||||
```
|
||||
|
||||
## Documentation updates
|
||||
|
||||
- README and README.zh-CN include CI control examples.
|
||||
- `skills/gitlink-ci` now prefers `ci +activate`, `ci +deactivate`, and
|
||||
`ci +authorize` instead of Raw API calls.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover:
|
||||
|
||||
- endpoint method/path mapping for activate, deactivate, and authorize;
|
||||
- dry-run behavior for state-changing commands;
|
||||
- `--yes` confirmation guards;
|
||||
- HTTP error propagation.
|
||||
|
||||
Suggested verification:
|
||||
|
||||
```bash
|
||||
go test ./shortcuts/ci ./shortcuts
|
||||
```
|
||||
|
||||
Full project verification:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
|
@ -11,7 +11,10 @@
|
|||
"cmd.branch.protect.short": "Set branch protection",
|
||||
"cmd.branch.short": "Branch operations",
|
||||
"cmd.branch.unprotect.short": "Remove branch protection",
|
||||
"cmd.ci.activate.short": "Activate repository CI",
|
||||
"cmd.ci.authorize.short": "Show repository CI authorization status",
|
||||
"cmd.ci.builds.short": "List CI builds",
|
||||
"cmd.ci.deactivate.short": "Deactivate repository CI",
|
||||
"cmd.ci.logs.short": "View build logs",
|
||||
"cmd.ci.restart.short": "Restart a build",
|
||||
"cmd.ci.short": "CI/CD operations",
|
||||
|
|
@ -126,7 +129,11 @@
|
|||
"flag.auth.token": "Login by pasting an existing token",
|
||||
"flag.branch.from": "Source branch or commit",
|
||||
"flag.branch.name": "Branch name",
|
||||
"flag.ci.activate_dry_run": "Preview the CI activation request without changing remote state",
|
||||
"flag.ci.activate_yes": "Confirm repository CI activation",
|
||||
"flag.ci.build": "Build number",
|
||||
"flag.ci.deactivate_dry_run": "Preview the CI deactivation request without changing remote state",
|
||||
"flag.ci.deactivate_yes": "Confirm repository CI deactivation",
|
||||
"flag.ci.stage": "Stage number",
|
||||
"flag.ci.step": "Step number",
|
||||
"flag.comment.body": "Comment body",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@
|
|||
"cmd.branch.protect.short": "设置分支保护",
|
||||
"cmd.branch.short": "分支操作",
|
||||
"cmd.branch.unprotect.short": "移除分支保护",
|
||||
"cmd.ci.activate.short": "激活仓库 CI",
|
||||
"cmd.ci.authorize.short": "显示仓库 CI 授权状态",
|
||||
"cmd.ci.builds.short": "列出 CI 构建",
|
||||
"cmd.ci.deactivate.short": "停用仓库 CI",
|
||||
"cmd.ci.logs.short": "查看构建日志",
|
||||
"cmd.ci.restart.short": "重启构建",
|
||||
"cmd.ci.short": "CI/CD 操作",
|
||||
|
|
@ -126,7 +129,11 @@
|
|||
"flag.auth.token": "通过粘贴已有 Token 登录",
|
||||
"flag.branch.from": "源分支或 Commit",
|
||||
"flag.branch.name": "分支名称",
|
||||
"flag.ci.activate_dry_run": "预览 CI 激活请求,不修改远端状态",
|
||||
"flag.ci.activate_yes": "确认激活仓库 CI",
|
||||
"flag.ci.build": "构建编号",
|
||||
"flag.ci.deactivate_dry_run": "预览 CI 停用请求,不修改远端状态",
|
||||
"flag.ci.deactivate_yes": "确认停用仓库 CI",
|
||||
"flag.ci.stage": "阶段编号",
|
||||
"flag.ci.step": "步骤编号",
|
||||
"flag.comment.body": "评论内容",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package ci
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
|
|
@ -96,9 +97,70 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "activate",
|
||||
Description: tr.T("cmd.ci.activate.short"),
|
||||
Flags: ciControlFlags(tr, "flag.ci.activate_dry_run", "flag.ci.activate_yes"),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runCIControl(ctx, "activate_ci", "POST", "activate", "activating CI changes repository CI state; run --dry-run first, then pass --yes to execute")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "deactivate",
|
||||
Description: tr.T("cmd.ci.deactivate.short"),
|
||||
Flags: ciControlFlags(tr, "flag.ci.deactivate_dry_run", "flag.ci.deactivate_yes"),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runCIControl(ctx, "deactivate_ci", "DELETE", "deactivate", "deactivating CI changes repository CI state; run --dry-run first, then pass --yes to execute")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authorize",
|
||||
Description: tr.T("cmd.ci.authorize.short"),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/ci_authorize", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ciControlFlags(tr *i18n.Translator, dryRunKey, yesKey string) []common.Flag {
|
||||
return []common.Flag{
|
||||
{Name: "dry-run", Usage: tr.T(dryRunKey), Bool: true, Default: "false"},
|
||||
{Name: "yes", Usage: tr.T(yesKey), Bool: true, Default: "false"},
|
||||
}
|
||||
}
|
||||
|
||||
func runCIControl(ctx *common.RuntimeContext, action, method, suffix, confirmMessage string) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
path := ctx.RepoPath() + "/" + suffix
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"action": action,
|
||||
"method": method,
|
||||
"path": path,
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
})
|
||||
}
|
||||
if ctx.Arg("yes") != "true" {
|
||||
return errors.New(confirmMessage)
|
||||
}
|
||||
env, err := ctx.CallAPI(method, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
|
|
|
|||
|
|
@ -127,6 +127,110 @@ func TestCIStop(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- activate / deactivate / authorize ---
|
||||
|
||||
func TestCIActivateDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("activate dry-run should not call remote API: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "activate", map[string]string{"dry-run": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("activate dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIActivateRequiresYes(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("activate without --yes should not call remote API: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "activate", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected activate to require --yes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIActivateWithYesCallsEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Fatalf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/owner/repo/activate.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "activate", map[string]string{"yes": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("activate failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDeactivateDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("deactivate dry-run should not call remote API: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "deactivate", map[string]string{"dry-run": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("deactivate dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDeactivateRequiresYes(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("deactivate without --yes should not call remote API: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "deactivate", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected deactivate to require --yes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDeactivateWithYesCallsEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
t.Fatalf("expected DELETE, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/owner/repo/deactivate.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "deactivate", map[string]string{"yes": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("deactivate failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAuthorizeCallsEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
t.Fatalf("expected GET, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/owner/repo/ci_authorize.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"authorized": true})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "authorize", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("authorize failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- HTTP error paths ---
|
||||
|
||||
func TestCIBuildsHTTPError(t *testing.T) {
|
||||
|
|
@ -180,3 +284,42 @@ func TestCIStopHTTPError(t *testing.T) {
|
|||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIActivateHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "activate", map[string]string{"yes": "true"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDeactivateHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "deactivate", map[string]string{"yes": "true"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIAuthorizeHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "authorize", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-ci
|
||||
version: 1.0.0
|
||||
description: "CI/CD 操作:查看构建列表、构建日志、重启/停止构建。当用户需要操作 GitLink CI 时触发。"
|
||||
version: 2.0.0
|
||||
description: "CI/CD 操作:查看构建列表、构建日志、重启/停止构建、启停仓库 CI、查看 CI 授权状态。当用户需要操作 GitLink CI 时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -24,6 +24,9 @@ metadata:
|
|||
| `ci +logs` | 构建日志 | 是 |
|
||||
| `ci +restart` | 重启构建 | 是 |
|
||||
| `ci +stop` | 停止构建 | 是 |
|
||||
| `ci +activate` | 激活仓库 CI,先 dry-run | 是 |
|
||||
| `ci +deactivate` | 停用仓库 CI,先 dry-run | 是 |
|
||||
| `ci +authorize` | 查看 CI 授权状态 | 是 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
|
|
@ -39,17 +42,21 @@ gitlink-cli ci +restart --build 42
|
|||
|
||||
# 停止构建
|
||||
gitlink-cli ci +stop --build 42
|
||||
|
||||
# 查看 CI 授权状态
|
||||
gitlink-cli ci +authorize --owner myuser --repo myrepo
|
||||
|
||||
# 激活仓库 CI,先预览再执行
|
||||
gitlink-cli ci +activate --owner myuser --repo myrepo --dry-run
|
||||
gitlink-cli ci +activate --owner myuser --repo myrepo --yes
|
||||
|
||||
# 停用仓库 CI,先预览再执行
|
||||
gitlink-cli ci +deactivate --owner myuser --repo myrepo --dry-run
|
||||
gitlink-cli ci +deactivate --owner myuser --repo myrepo --yes
|
||||
```
|
||||
|
||||
## Raw API 补充
|
||||
## 注意事项
|
||||
|
||||
```bash
|
||||
# 激活 CI
|
||||
gitlink-cli api POST /:owner/:repo/activate
|
||||
|
||||
# 停用 CI
|
||||
gitlink-cli api DELETE /:owner/:repo/deactivate
|
||||
|
||||
# CI 授权状态
|
||||
gitlink-cli api GET /:owner/:repo/ci_authorize
|
||||
```
|
||||
- `ci +activate` 和 `ci +deactivate` 会修改仓库 CI 状态,Agent 必须先执行 `--dry-run` 并获得用户确认,再加 `--yes`。
|
||||
- `ci +authorize` 是只读查询,可直接执行。
|
||||
- 在 Agent 场景建议统一加 `--format json`,便于解析构建状态和授权结果。
|
||||
|
|
|
|||
Loading…
Reference in New Issue